Browse Source

all contrib module security updates done

Bachir Soussi Chiadmi 3 years ago
parent
commit
0fbb9c4610
100 changed files with 12598 additions and 3444 deletions
  1. 2 0
      .gitignore
  2. 122 90
      sites/all/modules/contrib/admin/admin_menu/README.txt
  3. 3 4
      sites/all/modules/contrib/admin/admin_menu/admin_devel/admin_devel.info
  4. 4 5
      sites/all/modules/contrib/admin/admin_menu/admin_menu-rtl.css
  5. 8 4
      sites/all/modules/contrib/admin/admin_menu/admin_menu.admin.js
  6. 2 4
      sites/all/modules/contrib/admin/admin_menu/admin_menu.color.css
  7. 12 29
      sites/all/modules/contrib/admin/admin_menu/admin_menu.css
  8. 33 146
      sites/all/modules/contrib/admin/admin_menu/admin_menu.inc
  9. 3 4
      sites/all/modules/contrib/admin/admin_menu/admin_menu.info
  10. 4 3
      sites/all/modules/contrib/admin/admin_menu/admin_menu.install
  11. 8 16
      sites/all/modules/contrib/admin/admin_menu/admin_menu.js
  12. 42 52
      sites/all/modules/contrib/admin/admin_menu/admin_menu.map.inc
  13. 25 20
      sites/all/modules/contrib/admin/admin_menu/admin_menu.module
  14. 0 1
      sites/all/modules/contrib/admin/admin_menu/admin_menu.uid1.css
  15. 4 0
      sites/all/modules/contrib/admin/admin_menu/admin_menu_toolbar/admin_menu_toolbar-rtl.css
  16. 3 4
      sites/all/modules/contrib/admin/admin_menu/admin_menu_toolbar/admin_menu_toolbar.css
  17. 3 4
      sites/all/modules/contrib/admin/admin_menu/admin_menu_toolbar/admin_menu_toolbar.info
  18. 0 1
      sites/all/modules/contrib/admin/admin_menu/admin_menu_toolbar/admin_menu_toolbar.install
  19. 1 2
      sites/all/modules/contrib/admin/admin_menu/admin_menu_toolbar/admin_menu_toolbar.module
  20. 51 16
      sites/all/modules/contrib/admin/admin_menu/tests/admin_menu.test
  21. 333 268
      sites/all/modules/contrib/admin/admin_theme/LICENSE.txt
  22. 21 9
      sites/all/modules/contrib/admin/admin_theme/README.txt
  23. 9 14
      sites/all/modules/contrib/admin/admin_theme/admin_theme.api.php
  24. 4 7
      sites/all/modules/contrib/admin/admin_theme/admin_theme.info
  25. 14 13
      sites/all/modules/contrib/admin/admin_theme/admin_theme.install
  26. 45 36
      sites/all/modules/contrib/admin/admin_theme/admin_theme.module
  27. 0 122
      sites/all/modules/contrib/admin/admin_theme/translations/admin_theme.pot
  28. 0 122
      sites/all/modules/contrib/admin/admin_theme/translations/nl.po
  29. 310 0
      sites/all/modules/contrib/admin/backup_migrate/CHANGELOG.txt
  30. 179 57
      sites/all/modules/contrib/admin/backup_migrate/README.txt
  31. 43 0
      sites/all/modules/contrib/admin/backup_migrate/backup_migrate.advanced_settings.inc
  32. 41 10
      sites/all/modules/contrib/admin/backup_migrate/backup_migrate.css
  33. 43 8
      sites/all/modules/contrib/admin/backup_migrate/backup_migrate.info
  34. 759 100
      sites/all/modules/contrib/admin/backup_migrate/backup_migrate.install
  35. 118 51
      sites/all/modules/contrib/admin/backup_migrate/backup_migrate.js
  36. 15 10
      sites/all/modules/contrib/admin/backup_migrate/backup_migrate.module
  37. 145 50
      sites/all/modules/contrib/admin/backup_migrate/includes/backup_migrate.drush.inc
  38. 493 156
      sites/all/modules/contrib/admin/backup_migrate/includes/crud.inc
  39. 38 14
      sites/all/modules/contrib/admin/backup_migrate/includes/destinations.browser.inc
  40. 100 79
      sites/all/modules/contrib/admin/backup_migrate/includes/destinations.db.inc
  41. 266 105
      sites/all/modules/contrib/admin/backup_migrate/includes/destinations.db.mysql.inc
  42. 80 43
      sites/all/modules/contrib/admin/backup_migrate/includes/destinations.email.inc
  43. 145 78
      sites/all/modules/contrib/admin/backup_migrate/includes/destinations.file.inc
  44. 75 68
      sites/all/modules/contrib/admin/backup_migrate/includes/destinations.ftp.inc
  45. 534 266
      sites/all/modules/contrib/admin/backup_migrate/includes/destinations.inc
  46. 37 28
      sites/all/modules/contrib/admin/backup_migrate/includes/destinations.s3.inc
  47. 185 64
      sites/all/modules/contrib/admin/backup_migrate/includes/files.inc
  48. 131 37
      sites/all/modules/contrib/admin/backup_migrate/includes/filters.backup_restore.inc
  49. 33 26
      sites/all/modules/contrib/admin/backup_migrate/includes/filters.compression.inc
  50. 136 38
      sites/all/modules/contrib/admin/backup_migrate/includes/filters.encryption.inc
  51. 88 48
      sites/all/modules/contrib/admin/backup_migrate/includes/filters.inc
  52. 23 14
      sites/all/modules/contrib/admin/backup_migrate/includes/filters.statusnotify.inc
  53. 119 51
      sites/all/modules/contrib/admin/backup_migrate/includes/filters.utils.inc
  54. 529 0
      sites/all/modules/contrib/admin/backup_migrate/includes/locations.inc
  55. 145 70
      sites/all/modules/contrib/admin/backup_migrate/includes/profiles.inc
  56. 577 118
      sites/all/modules/contrib/admin/backup_migrate/includes/schedules.inc
  57. 314 0
      sites/all/modules/contrib/admin/backup_migrate/includes/sources.archivesource.inc
  58. 330 0
      sites/all/modules/contrib/admin/backup_migrate/includes/sources.db.inc
  59. 593 0
      sites/all/modules/contrib/admin/backup_migrate/includes/sources.db.mysql.inc
  60. 335 0
      sites/all/modules/contrib/admin/backup_migrate/includes/sources.filesource.inc
  61. 274 0
      sites/all/modules/contrib/admin/backup_migrate/includes/sources.inc
  62. 301 0
      sites/all/modules/contrib/admin/backup_migrate/tests/BmTestBase.test
  63. 276 0
      sites/all/modules/contrib/admin/backup_migrate/tests/BmTestBasics.test
  64. 104 0
      sites/all/modules/contrib/admin/backup_migrate/tests/BmTestCtools.test
  65. 207 0
      sites/all/modules/contrib/admin/backup_migrate/tests/BmTestEmail.test
  66. 170 0
      sites/all/modules/contrib/admin/backup_migrate/tests/BmTestProfiles.test
  67. 141 0
      sites/all/modules/contrib/admin/backup_migrate/tests/BmTestUpdate7310.test
  68. 13 0
      sites/all/modules/contrib/admin/backup_migrate/tests/bm_test.info
  69. 121 0
      sites/all/modules/contrib/admin/backup_migrate/tests/bm_test.module
  70. 7 0
      sites/all/modules/contrib/admin/backup_migrate/tests/files/test.txt
  71. 55 27
      sites/all/modules/contrib/admin/google_analytics/README.txt
  72. 1 1
      sites/all/modules/contrib/admin/google_analytics/googleanalytics.admin.inc
  73. 10 1
      sites/all/modules/contrib/admin/google_analytics/googleanalytics.admin.js
  74. 213 0
      sites/all/modules/contrib/admin/google_analytics/googleanalytics.debug.js
  75. 4 5
      sites/all/modules/contrib/admin/google_analytics/googleanalytics.info
  76. 45 36
      sites/all/modules/contrib/admin/google_analytics/googleanalytics.install
  77. 139 37
      sites/all/modules/contrib/admin/google_analytics/googleanalytics.js
  78. 222 127
      sites/all/modules/contrib/admin/google_analytics/googleanalytics.module
  79. 512 135
      sites/all/modules/contrib/admin/google_analytics/googleanalytics.test
  80. 121 0
      sites/all/modules/contrib/admin/google_analytics/googleanalytics.test.js
  81. 0 0
      sites/all/modules/contrib/admin/google_analytics/googleanalytics.variable.inc
  82. 187 35
      sites/all/modules/contrib/admin/module_filter/CHANGELOG.txt
  83. 107 0
      sites/all/modules/contrib/admin/module_filter/README.txt
  84. 23 0
      sites/all/modules/contrib/admin/module_filter/css/dynamic_position.css
  85. 23 1
      sites/all/modules/contrib/admin/module_filter/css/module_filter.css
  86. 54 0
      sites/all/modules/contrib/admin/module_filter/css/module_filter_tab-rtl.css
  87. 218 95
      sites/all/modules/contrib/admin/module_filter/css/module_filter_tab.css
  88. 53 0
      sites/all/modules/contrib/admin/module_filter/css/modules.css
  89. 18 0
      sites/all/modules/contrib/admin/module_filter/css/update_status.css
  90. BIN
      sites/all/modules/contrib/admin/module_filter/images/collapsed.png
  91. BIN
      sites/all/modules/contrib/admin/module_filter/images/expanded.png
  92. 36 21
      sites/all/modules/contrib/admin/module_filter/js/dynamic_position.js
  93. 248 94
      sites/all/modules/contrib/admin/module_filter/js/module_filter.js
  94. 505 227
      sites/all/modules/contrib/admin/module_filter/js/module_filter_tab.js
  95. 181 0
      sites/all/modules/contrib/admin/module_filter/js/modules.js
  96. 67 0
      sites/all/modules/contrib/admin/module_filter/js/permissions.js
  97. 117 0
      sites/all/modules/contrib/admin/module_filter/js/update_status.js
  98. 78 11
      sites/all/modules/contrib/admin/module_filter/module_filter.admin.inc
  99. 4 5
      sites/all/modules/contrib/admin/module_filter/module_filter.info
  100. 28 1
      sites/all/modules/contrib/admin/module_filter/module_filter.install

+ 2 - 0
.gitignore

@@ -6,3 +6,5 @@ sites/*/files
 sites/*/private
 
 vendor/*
+
+*.tar.gz

+ 122 - 90
sites/all/modules/contrib/admin/admin_menu/README.txt

@@ -1,65 +1,84 @@
+CONTENTS OF THIS FILE
+---------------------
 
--- SUMMARY --
+ * Introduction
+ * Requirements
+ * Installation
+ * Configuration
+ * Customization
+ * Troubleshooting
+ * FAQ
+ * Maintainers
+
+
+INTRODUCTION
+------------
 
 The Administration menu module displays the entire administrative menu tree (and
 most local tasks) in a drop-down menu, providing administrators one- or
 two-click access to most pages.  Other modules may also add menu links to the
 menu using hook_admin_menu_output_alter().
 
-For a full description of the module, visit the project page:
-  http://drupal.org/project/admin_menu
+For a full description of the project visit the project page:
+http://drupal.org/project/admin_menu
 
 To submit bug reports and feature suggestions, or to track changes:
-  http://drupal.org/project/issues/admin_menu
+http://drupal.org/project/issues/admin_menu
 
 
--- REQUIREMENTS --
+REQUIREMENTS
+------------
 
-None.
+No special requirements
 
 
--- INSTALLATION --
+INSTALLATION
+------------
 
-* Install as usual, see http://drupal.org/node/70151 for further information.
+Install as you would normally install a contributed Drupal. See:
+https://drupal.org/documentation/install/modules-themes/modules-7 for further
+information.
 
-* You likely want to disable Toolbar module, since its output clashes with
-  Administration menu.
+ * You likely want to disable Toolbar module, since its output clashes with
+ Administration menu.
 
 
--- CONFIGURATION --
+CONFIGURATION
+-------------
 
-* Configure user permissions in Administration » People » Permissions:
+ * Configure user permissions in Administration » People » Permissions:
 
-  - Use the administration pages and help (System module)
+   - Use the administration pages and help (System module)
 
-    The top-level administration categories require this permission to be
-    accessible. The administration menu will be empty unless this permission is
-    granted.
+     The top-level administration categories require this permission to be
+     accessible. The administration menu will be empty unless this permission is
+     granted.
 
-  - Access administration menu
+   - Access administration menu
 
-    Users in roles with the "Access administration menu" permission will see
-    the administration menu at the top of each page.
+     Users in roles with the "Access administration menu" permission will see
+     the administration menu at the top of each page.
 
-  - Display Drupal links
+   - Display Drupal links
 
-    Users in roles with the "Display drupal links" permission will receive
-    links to drupal.org issue queues for all enabled contributed modules. The
-    issue queue links appear under the administration menu icon.
+     Users in roles with the "Display drupal links" permission will receive
+     links to drupal.org issue queues for all enabled contributed modules. The
+     issue queue links appear under the administration menu icon.
 
-  Note that the menu items displayed in the administration menu depend on the
-  actual permissions of the viewing user. For example, the "People" menu item
-  is not displayed to a user who is not a member of a role with the "Administer
-  users" permission.
+     Note that the menu items displayed in the administration menu depend on the
+     actual permissions of the viewing user. For example, the "People" menu item
+     is not displayed to a user who is not a member of a role with the
+     "Administer users" permission.
 
-* Customize the menu settings in Administration » Configuration and modules »
-  Administration » Administration menu.
+ * Customize the menu settings in Administration » Configuration and modules »
+   Administration » Administration menu.
 
-* To prevent administrative menu items from appearing twice, you may hide the
-  "Management" menu block.
+ * To prevent administrative menu items from appearing twice, you may hide the
+   "Management" menu block.
 
 
--- CUSTOMIZATION --
+CUSTOMIZATION
+-------------
 
 * To override the default administration menu icon, you may:
 
@@ -82,12 +101,13 @@ None.
   body #admin-menu { font-size: 10px; }
 
 
--- TROUBLESHOOTING --
+TROUBLESHOOTING
+-------------
 
 * If the menu does not display, check the following:
 
-  - Are the "Access administration menu" and "Use the administration pages and help"
-    permissions enabled for the appropriate roles?
+  - Are the "Access administration menu" and "Use the administration pages and
+    help" permissions enabled for the appropriate roles?
 
   - Does html.tpl.php of your theme output the $page_bottom variable?
 
@@ -99,90 +119,102 @@ None.
   See http://drupal.org/node/195386 for further information.
 
 
--- FAQ --
+FAQ
+---
+
+ Q: When the administration menu module is enabled, blank space is added to the
+    bottom of my theme. Why?
 
-Q: When the administration menu module is enabled, blank space is added to the
-   bottom of my theme. Why?
+ A: This is caused by a long list of links to module issue queues at Drupal.org.
+    Use Administer >> User management >> Permissions to disable the "display
+    drupal links" permission for all appropriate roles. Note that since UID 1
+    automatically receives all permissions, the list of issue queue links cannot
+    be disabled for UID 1.
 
-A: This is caused by a long list of links to module issue queues at Drupal.org.
-   Use Administer >> User management >> Permissions to disable the "display
-   drupal links" permission for all appropriate roles. Note that since UID 1
-   automatically receives all permissions, the list of issue queue links cannot
-   be disabled for UID 1.
 
+ Q: After upgrading to 6.x-1.x, the menu disappeared. Why?
 
-Q: After upgrading to 6.x-1.x, the menu disappeared. Why?
+ A: You may need to regenerate your menu. Visit
+    http://example.com/admin/build/modules to regenerate your menu (substitute
+    your site name for example.com).
 
-A: You may need to regenerate your menu. Visit
-   http://example.com/admin/build/modules to regenerate your menu (substitute
-   your site name for example.com).
 
+ Q: Can I configure the administration menu module to display another menu (like
+    the Navigation menu, for instance)?
 
-Q: Can I configure the administration menu module to display another menu (like
-   the Navigation menu, for instance)?
+ A: No. As the name implies, administration menu module is for administrative
+    menu links only. However, you can copy and paste the contents of
+    admin_menu.css into your theme's stylesheet and replace #admin-menu with any
+    other menu block id (#block-menu-1, for example).
 
-A: No. As the name implies, administration menu module is for administrative
-   menu links only. However, you can copy and paste the contents of
-   admin_menu.css into your theme's stylesheet and replace #admin-menu with any
-   other menu block id (#block-menu-1, for example).
 
+ Q: Sometimes, the user counter displays a lot of anonymous users, but no spike
+    of users or requests appear in Google Analytics or other tracking tools.
 
-Q: Sometimes, the user counter displays a lot of anonymous users, but no spike
-   of users or requests appear in Google Analytics or other tracking tools.
+ A: If your site was concurrently spidered by search-engine robots, it may have
+    a significant number of anonymous users for a short time. Most web tracking
+    tools like Google Analytics automatically filter out these requests.
 
-A: If your site was concurrently spidered by search-engine robots, it may have
-   a significant number of anonymous users for a short time. Most web tracking
-   tools like Google Analytics automatically filter out these requests.
 
+ Q: I enabled "Aggregate and compress CSS files", but admin_menu.css is still
+    there. Is this normal?
 
-Q: I enabled "Aggregate and compress CSS files", but admin_menu.css is still
-   there. Is this normal?
+ A: Yes, this is the intended behavior. the administration menu module only
+    loads its stylesheet as needed (i.e., on page requests by logged-on,
+    administrative users).
 
-A: Yes, this is the intended behavior. the administration menu module only loads
-   its stylesheet as needed (i.e., on page requests by logged-on, administrative
-   users).
 
+ Q: Why are sub-menus not visible in Opera?
 
-Q: Why are sub-menus not visible in Opera?
+ A: In the Opera browser preferences under "web pages" there is an option to fit
+    to width. By disabling this option, sub-menus in the administration menu
+    should appear.
 
-A: In the Opera browser preferences under "web pages" there is an option to fit
-   to width. By disabling this option, sub-menus in the administration menu
-   should appear.
 
+ Q: How can the administration menu be hidden on certain pages?
 
-Q: How can the administration menu be hidden on certain pages?
+ A: You can suppress it by simply calling the following function in PHP:
+    module_invoke('admin_menu', 'suppress');
 
-A: You can suppress it by simply calling the following function in PHP:
+    However, this needs to happen as early as possible in the page request, so
+    placing it in the theming layer (resp. a page template file) is too late.
+    Ideally, the function is called in hook_init() in a custom module.  If you
+    do not have a custom module, placing it into some conditional code at the
+    top of template.php may work out, too.
 
-     module_invoke('admin_menu', 'suppress');
 
-   However, this needs to happen as early as possible in the page request, so
-   placing it in the theming layer (resp. a page template file) is too late.
-   Ideally, the function is called in hook_init() in a custom module.  If you do
-   not have a custom module, placing it into some conditional code at the top of
-   template.php may work out, too.
+Q: What does the "Administration Development Tools" module do?
 
+A: The Administration Development Tools adds a jQuery Debugger which allows
+   a developer to debug and inspect arbitrary data/variables in Firebug's
+   console, and also to access them again in the global window object
+   (optionally using a named identifier, e.g. window.debug.myValue).
+   Chainable via jQuery. Especially useful for re-accessing and debugging
+   selected data via Firebug's console.
 
--- CONTACT --
+
+MAINTAINERS
+-----------
 
 Current maintainers:
-* Daniel F. Kudwien (sun) - http://drupal.org/user/54136
-* Peter Wolanin (pwolanin) - http://drupal.org/user/49851
-* Stefan M. Kudwien (smk-ka) - http://drupal.org/user/48898
-* Dave Reid (Dave Reid) - http://drupal.org/user/53892
+ * Daniel F. Kudwien (sun) - http://drupal.org/user/54136
+ * Peter Wolanin (pwolanin) - http://drupal.org/user/49851
+ * Stefan M. Kudwien (smk-ka) - http://drupal.org/user/48898
+ * Dave Reid (Dave Reid) - http://drupal.org/user/53892
+ * Truls S. Yggeseth (truls1502) - http://drupal.org/user/325866
+ * Sebastian Siemssen (fubhy) - https://www.drupal.org/user/761344
 
 Major rewrite for Drupal 6 by Peter Wolanin (pwolanin).
 
 This project has been sponsored by:
-* UNLEASHED MIND
-  Specialized in consulting and planning of Drupal powered sites, UNLEASHED
-  MIND offers installation, development, theming, customization, and hosting
-  to get you started. Visit http://www.unleashedmind.com for more information.
-
-* Lullabot
-  Friendly Drupal experts providing professional consulting & education
-  services. Visit http://www.lullabot.com for more information.
+ * UNLEASHED MIND
+   Specialized in consulting and planning of Drupal powered sites, UNLEASHED
+   MIND offers installation, development, theming, customization, and hosting
+   to get you started. Visit http://www.unleashedmind.com for more information.
 
-* Acquia
-  Commercially Supported Drupal. Visit http://acquia.com for more information.
+ * Lullabot
+   Friendly Drupal experts providing professional consulting & education
+   services. Visit http://www.lullabot.com for more information.
 
+ * Acquia
+   Commercially Supported Drupal. Visit http://acquia.com for more information.

+ 3 - 4
sites/all/modules/contrib/admin/admin_menu/admin_devel/admin_devel.info

@@ -4,9 +4,8 @@ package = Administration
 core = 7.x
 scripts[] = admin_devel.js
 
-; Information added by drupal.org packaging script on 2013-01-31
-version = "7.x-3.0-rc4"
+; Information added by Drupal.org packaging script on 2018-12-03
+version = "7.x-3.0-rc6"
 core = "7.x"
 project = "admin_menu"
-datestamp = "1359651687"
-
+datestamp = "1543859284"

+ 4 - 5
sites/all/modules/contrib/admin/admin_menu/admin_menu-rtl.css

@@ -1,4 +1,3 @@
-
 #admin-menu {
   text-align: right;
 }
@@ -25,7 +24,7 @@
   border-right: 0;
 }
 #admin-menu .dropdown .admin-menu-tab a {
-  border-left: 1px solid #52565E;
+  border-left: 1px solid #52565e;
   border-right: 0;
 }
 #admin-menu .dropdown li li a {
@@ -42,13 +41,13 @@
 /* Second-level lists */
 #admin-menu .dropdown li ul {
   left: auto;
-  right: -999em;
+  right: auto;
 }
 
 /* Third-and-above-level lists */
 #admin-menu .dropdown li li.expandable ul {
-  margin-left: 0;
-  margin-right: 160px;
+  margin-left: 0 !important;
+  margin-right: 160px !important;
 }
 
 /* Lists nested under hovered list items */

+ 8 - 4
sites/all/modules/contrib/admin/admin_menu/admin_menu.admin.js

@@ -1,4 +1,8 @@
-(function($) {
+/**
+ * @file
+ */
+
+(function ($) {
 
 /**
  * Live preview of Administration menu components.
@@ -45,9 +49,9 @@ Drupal.behaviors.adminMenuPermissionsSetupHelp = {
               // Figure out which is the other, check whether it still disabled,
               // and if so, ask whether to auto-enable it.
               var other = (this == $admin[index] ? $menu[index] : $admin[index]);
-              if (!other.checked && confirm(Drupal.t('Also allow !name role to !permission?', {
-                '!name': $roles[index].textContent,
-                '!permission': (this == $admin[index] ? menuPermission : adminPermission)
+              if (!other.checked && confirm(Drupal.t('Also allow @name role to @permission?', {
+                '@name': $roles[index].textContent,
+                '@permission': (this == $admin[index] ? menuPermission : adminPermission)
               }))) {
                 other.checked = 'checked';
               }

+ 2 - 4
sites/all/modules/contrib/admin/admin_menu/admin_menu.color.css

@@ -1,4 +1,3 @@
-
 /**
  * @file
  * Administration menu color override.
@@ -17,7 +16,7 @@
   border-right-color: #a91f1f;
 }
 #admin-menu ul li.admin-menu-tab a {
-  border-right-color: #52565E;
+  border-right-color: #52565e;
 }
 #admin-menu li li a {
   border-top-color: #801f1f;
@@ -32,8 +31,7 @@
 #admin-menu li li.expandable {
   background-color: #b93f3f;
 }
-#admin-menu li li:hover,
-#admin-menu li li.iehover {
+#admin-menu li li:hover {
   background-color: #690f0f;
 }
 #admin-menu li li.expandable:hover a,

+ 12 - 29
sites/all/modules/contrib/admin/admin_menu/admin_menu.css

@@ -1,4 +1,3 @@
-
 /**
  * @file
  * Administration menu.
@@ -16,6 +15,7 @@
   position: absolute;
   text-align: left;
   top: 0;
+  height: 30px;
   width: 100%;
 }
 #admin-menu-wrapper {
@@ -62,7 +62,7 @@ body.admin-menu {
 #admin-menu li > span {
   background: transparent none;
   border: none;
-  color: #EEE;
+  color: #eee;
   font-weight: normal;
   text-align: left; /* LTR */
   text-decoration: none;
@@ -74,7 +74,7 @@ body.admin-menu {
   padding: 4px 8px;
 }
 #admin-menu .dropdown .admin-menu-tab a {
-  border-right: 1px solid #52565E; /* LTR */
+  border-right: 1px solid #52565e; /* LTR */
 }
 #admin-menu .dropdown li li a {
   border-right: none; /* LTR */
@@ -98,6 +98,7 @@ body.admin-menu {
 #admin-menu .dropdown li li {
   background: #202020;
   filter: Alpha(opacity=88);
+  float: none;
   opacity: 0.88;
   width: 160px; /* Required for Opera */
 }
@@ -126,11 +127,7 @@ body.admin-menu {
 #admin-menu .dropdown li:hover ul ul,
 #admin-menu .dropdown li:hover ul ul ul,
 #admin-menu .dropdown li:hover ul ul ul ul,
-#admin-menu .dropdown li:hover ul ul ul ul ul,
-#admin-menu .dropdown li.iehover ul ul,
-#admin-menu .dropdown li.iehover ul ul ul,
-#admin-menu .dropdown li.iehover ul ul ul ul,
-#admin-menu .dropdown li.iehover ul ul ul ul ul {
+#admin-menu .dropdown li:hover ul ul ul ul ul {
   display: none;
   left: -999em; /* LTR */
 }
@@ -140,12 +137,7 @@ body.admin-menu {
 #admin-menu .dropdown li li:hover ul,
 #admin-menu .dropdown li li li:hover ul,
 #admin-menu .dropdown li li li li:hover ul,
-#admin-menu .dropdown li li li li li:hover ul,
-#admin-menu .dropdown li.iehover ul,
-#admin-menu .dropdown li li.iehover ul,
-#admin-menu .dropdown li li li.iehover ul,
-#admin-menu .dropdown li li li li.iehover ul,
-#admin-menu .dropdown li li li li li.iehover ul {
+#admin-menu .dropdown li li li li li:hover ul {
   display: block;
   left: auto; /* LTR */
 }
@@ -155,36 +147,27 @@ body.admin-menu {
 
 /* Second-and-more-level hovering */
 #admin-menu .dropdown li li.expandable {
-  background: #45454A url(images/arrow.png) no-repeat 145px 6px;
+  background: #45454a url(images/arrow.png) no-repeat 145px 6px;
 }
-#admin-menu .dropdown li li:hover,
-#admin-menu .dropdown li li.iehover {
+#admin-menu .dropdown li li:hover {
   background-color: #111;
 }
 #admin-menu .dropdown li li:hover a,
 #admin-menu .dropdown li li:hover li:hover a,
 #admin-menu .dropdown li li:hover li:hover li:hover a {
-  color: #FFF;
+  color: #fff;
 }
 #admin-menu .dropdown li li.expandable:hover a,
 #admin-menu .dropdown li li.expandable:hover li.expandable:hover a {
   border-color: #444;
-  color: #EEE;
+  color: #eee;
 }
 #admin-menu .dropdown li li.expandable:hover li a,
 #admin-menu .dropdown li li.expandable:hover li.expandable:hover li a {
   border-color: #323232;
 }
-#admin-menu .dropdown li li:hover li a,
-#admin-menu .dropdown li li.iehover li a,
-#admin-menu .dropdown li li.iehover li.iehover li a {
-  color: #EEE;
-}
-#admin-menu .dropdown li li.iehover a,
-#admin-menu .dropdown li li.iehover li.iehover a,
-#admin-menu .dropdown li li.iehover li.iehover li.iehover a {
-  color: #FFF;
-  width: 90%; /* IE */
+#admin-menu .dropdown li li:hover li a {
+  color: #eee;
 }
 
 /* Search form */

+ 33 - 146
sites/all/modules/contrib/admin/admin_menu/admin_menu.inc

@@ -97,7 +97,7 @@ function admin_menu_tree_dynamic(array $expand_map) {
   $db_or = db_or();
   foreach ($plids as $path_plids) {
     $db_and = db_and();
-    // plids with value 0 may be ignored.
+    // Plids with value 0 may be ignored.
     foreach (array_filter($path_plids) as $column => $plid) {
       $db_and->condition($column, $plid);
     }
@@ -204,7 +204,7 @@ function admin_menu_merge_tree(array &$tree, array $tree_dynamic, array $expand_
         foreach ($load_functions as $index => $function) {
           if ($function) {
             if (is_array($function)) {
-              list($function,) = each($function);
+              $function = key($function);
             }
             // Add the loader function name minus "_load".
             $placeholder = '%' . substr($function, 0, -5);
@@ -269,7 +269,7 @@ function admin_menu_translate($router_item, $map) {
     // replace any other.
     // @todo Doing this instead leads to plenty of duplicate links below
     //   admin/structure/menu; likely a hidden recursion problem.
-    // $router_item['mlid'] = $router_item['href'] . $router_item['mlid'];
+    // $router_item['mlid'] = $router_item['href'] . $router_item['mlid'];.
     $router_item['mlid'] = $router_item['href'];
     // Turn menu callbacks into regular menu items to make them visible.
     if ($router_item['type'] == MENU_CALLBACK) {
@@ -278,10 +278,12 @@ function admin_menu_translate($router_item, $map) {
 
     // @see _menu_tree_check_access()
     $key = (50000 + $router_item['weight']) . ' ' . $router_item['title'] . ' ' . $router_item['mlid'];
-    return array($key => array(
-      'link' => $router_item,
-      'below' => array(),
-    ));
+    return array(
+      $key => array(
+        'link' => $router_item,
+        'below' => array(),
+      ),
+    );
   }
 
   return array();
@@ -461,20 +463,22 @@ function admin_menu_links_icon() {
     '#access' => user_access('display drupal links'),
     '#href' => 'http://drupal.org',
   );
-  // Add links to project issue queues.
-  foreach (module_list(FALSE, TRUE) as $module) {
-    $info = drupal_parse_info_file(drupal_get_path('module', $module) . '/' . $module . '.info');
-    if (!isset($info['project']) || isset($links['icon']['drupal.org'][$info['project']])) {
-      continue;
+  if (variable_get('admin_menu_issue_queues', TRUE)) {
+    // Add links to project issue queues.
+    foreach (module_list(FALSE, TRUE) as $module) {
+      $info = drupal_parse_info_file(drupal_get_path('module', $module) . '/' . $module . '.info');
+      if (!isset($info['project']) || isset($links['icon']['drupal.org'][$info['project']])) {
+        continue;
+      }
+      $links['icon']['drupal.org'][$info['project']] = array(
+        '#title' => t('@project issue queue', array('@project' => $info['name'])),
+        '#weight' => ($info['project'] == 'drupal' ? -10 : 0),
+        '#href' => 'http://drupal.org/project/issues/' . $info['project'],
+        '#options' => array(
+          'query' => array('version' => (isset($info['core']) ? $info['core'] : 'All')),
+        ),
+      );
     }
-    $links['icon']['drupal.org'][$info['project']] = array(
-      '#title' => t('@project issue queue', array('@project' => $info['name'])),
-      '#weight' => ($info['project'] == 'drupal' ? -10 : 0),
-      '#href' => 'http://drupal.org/project/issues/' . $info['project'],
-      '#options' => array(
-        'query' => array('version' => (isset($info['core']) ? $info['core'] : 'All')),
-      ),
-    );
   }
   // Add items to flush caches.
   $links['icon']['flush-cache'] = array(
@@ -497,18 +501,6 @@ function admin_menu_links_icon() {
     );
   }
 
-  // Add link to toggle developer modules (performance).
-  $saved_state = variable_get('admin_menu_devel_modules_enabled', NULL);
-  $links['icon']['toggle-modules'] = array(
-    '#title' => isset($saved_state) ? t('Enable developer modules') : t('Disable developer modules'),
-    '#weight' => 88,
-    '#access' => user_access('administer modules'),
-    '#href' => 'admin_menu/toggle-modules',
-    '#options' => array(
-      'query' => $destination + array('token' => drupal_get_token('admin_menu/toggle-modules')),
-    ),
-  );
-
   // Add Devel module menu links.
   if (module_exists('devel')) {
     $devel_tree = menu_build_tree('devel');
@@ -582,7 +574,7 @@ function admin_menu_links_users() {
     '#description' => t('Current anonymous / authenticated users'),
     '#weight' => -90,
     '#attributes' => array('class' => array('admin-menu-action', 'admin-menu-users')),
-    '#href' => (user_access('administer users') ? 'admin/people/people' : 'user'),
+    '#href' => (user_access('administer users') ? 'admin/people' : 'user'),
   );
   return $links;
 }
@@ -670,7 +662,7 @@ function admin_menu_theme_settings() {
     '#default_value' => variable_get('admin_menu_tweak_modules', 0),
   );
   if (module_exists('util')) {
-    $form['tweaks']['admin_menu_tweak_modules']['#description'] .= '<br /><strong>' . t('If the Utility module was installed for this purpose, it can be safely disabled and uninstalled.') . '</strong>';
+    $form['tweaks']['admin_menu_tweak_modules']['#description'] = '<br /><strong>' . t('If the Utility module was installed for this purpose, it can be safely disabled and uninstalled.') . '</strong>';
   }
   $form['tweaks']['admin_menu_tweak_permissions'] = array(
     '#type' => 'checkbox',
@@ -697,28 +689,10 @@ function admin_menu_theme_settings() {
     '#title' => t('Cache menu in client-side browser'),
     '#default_value' => variable_get('admin_menu_cache_client', 1),
   );
-  // Fetch all available modules manually, since module_list() only returns
-  // currently enabled modules, which makes this setting pointless if developer
-  // modules are currently disabled.
-  $all_modules = array();
-  $result = db_query("SELECT name, filename, info FROM {system} WHERE type = 'module' ORDER BY name ASC");
-  foreach ($result as $module) {
-    if (file_exists($module->filename)) {
-      $info = unserialize($module->info);
-      $all_modules[$module->name] = $info['name'];
-    }
-  }
-  $devel_modules = variable_get('admin_menu_devel_modules', _admin_menu_developer_modules());
-  $devel_modules = array_intersect_key($all_modules, array_flip($devel_modules));
-  $form['performance']['admin_menu_devel_modules_skip'] = array(
-    '#type' => 'checkboxes',
-    '#title' => t('Developer modules to keep enabled'),
-    '#default_value' => variable_get('admin_menu_devel_modules_skip', array()),
-    '#options' => $devel_modules,
-    '#access' => !empty($devel_modules),
-    '#description' => t('The selected modules will not be disabled when the link %disable-developer-modules below the icon in the menu is invoked.', array(
-      '%disable-developer-modules' => t('Disable developer modules'),
-    )),
+  $form['performance']['admin_menu_issue_queues'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Show Issue Queue links in icon menu'),
+    '#default_value' => variable_get('admin_menu_issue_queues', 1),
   );
 
   return system_settings_form($form);
@@ -783,94 +757,6 @@ function _admin_menu_form_devel_admin_settings_alter(&$form, $form_state) {
   );
 }
 
-/**
- * Menu callback; Enable/disable developer modules.
- *
- * This can save up to 150ms on each uncached page request.
- */
-function admin_menu_toggle_modules() {
-  if (!isset($_GET['token']) || !drupal_valid_token($_GET['token'], current_path())) {
-    return MENU_ACCESS_DENIED;
-  }
-
-  $rebuild = FALSE;
-  $saved_state = variable_get('admin_menu_devel_modules_enabled', NULL);
-  if (isset($saved_state)) {
-    // Re-enable modules that were enabled before.
-    module_enable($saved_state);
-    variable_del('admin_menu_devel_modules_enabled');
-    drupal_set_message(t('Enabled these modules: !module-list.', array('!module-list' => implode(', ', $saved_state))));
-    $rebuild = TRUE;
-  }
-  else {
-    // Allow site admins to override this variable via settings.php.
-    $devel_modules = variable_get('admin_menu_devel_modules', _admin_menu_developer_modules());
-    // Store currently enabled modules in a variable.
-    $devel_modules = array_intersect(module_list(FALSE, FALSE), $devel_modules);
-    $devel_modules = array_diff($devel_modules, variable_get('admin_menu_devel_modules_skip', array()));
-    if (!empty($devel_modules)) {
-      variable_set('admin_menu_devel_modules_enabled', $devel_modules);
-      // Disable developer modules.
-      module_disable($devel_modules);
-      drupal_set_message(t('Disabled these modules: !module-list.', array('!module-list' => implode(', ', $devel_modules))));
-      $rebuild = TRUE;
-    }
-    else {
-      drupal_set_message(t('No developer modules are enabled.'));
-    }
-  }
-  if ($rebuild) {
-    // Make sure everything is rebuilt, basically a combination of the calls
-    // from system_modules() and system_modules_submit().
-    drupal_theme_rebuild();
-    menu_rebuild();
-    cache_clear_all('schema', 'cache');
-    cache_clear_all();
-    drupal_clear_css_cache();
-    drupal_clear_js_cache();
-    // Synchronize to catch any actions that were added or removed.
-    actions_synchronize();
-    // Finally, flush admin_menu's cache.
-    admin_menu_flush_caches();
-  }
-  drupal_goto();
-}
-
-/**
- * Helper function to return a default list of developer modules.
- */
-function _admin_menu_developer_modules() {
-  return array(
-    'admin_devel',
-    'cache_disable',
-    'coder',
-    'content_copy',
-    'context_ui',
-    'debug',
-    'delete_all',
-    'demo',
-    'devel',
-    'devel_node_access',
-    'devel_themer',
-    'field_ui',
-    'fontyourface_ui',
-    'form_controller',
-    'imagecache_ui',
-    'journal',
-    'l10n_client',
-    'l10n_update',
-    'macro',
-    'rules_admin',
-    'stringoverrides',
-    'trace',
-    'upgrade_status',
-    'user_display_ui',
-    'util',
-    'views_ui',
-    'views_theme_wizard',
-  );
-}
-
 /**
  * Flush all caches or a specific one.
  *
@@ -886,18 +772,20 @@ function admin_menu_flush_cache($name = NULL) {
     if (!isset($caches[$name])) {
       return MENU_NOT_FOUND;
     }
+    $message = t('@title cache cleared.', array('@title' => $caches[$name]['title']));
   }
   else {
     $caches[$name] = array(
       'title' => t('Every'),
       'callback' => 'drupal_flush_all_caches',
     );
+    $message = t('All caches cleared.');
   }
   // Pass the cache to flush forward to the callback.
   $function = $caches[$name]['callback'];
   $function($name);
 
-  drupal_set_message(t('!title cache cleared.', array('!title' => $caches[$name]['title'])));
+  drupal_set_message($message);
 
   // The JavaScript injects a destination request parameter pointing to the
   // originating page, so the user is redirected back to that page. Without
@@ -1030,4 +918,3 @@ function template_preprocess_admin_menu_icon(&$variables) {
 function theme_admin_menu_icon($variables) {
   return '<img class="admin-menu-icon" src="' . $variables['src'] . '" width="16" height="16" alt="' . $variables['alt'] . '" />';
 }
-

+ 3 - 4
sites/all/modules/contrib/admin/admin_menu/admin_menu.info

@@ -8,9 +8,8 @@ configure = admin/config/administration/admin_menu
 dependencies[] = system (>7.10)
 files[] = tests/admin_menu.test
 
-; Information added by drupal.org packaging script on 2013-01-31
-version = "7.x-3.0-rc4"
+; Information added by Drupal.org packaging script on 2018-12-03
+version = "7.x-3.0-rc6"
 core = "7.x"
 project = "admin_menu"
-datestamp = "1359651687"
-
+datestamp = "1543859284"

+ 4 - 3
sites/all/modules/contrib/admin/admin_menu/admin_menu.install

@@ -33,9 +33,6 @@ function admin_menu_install() {
 function admin_menu_uninstall() {
   // Delete variables.
   variable_del('admin_menu_components');
-  variable_del('admin_menu_devel_modules');
-  variable_del('admin_menu_devel_modules_enabled');
-  variable_del('admin_menu_devel_modules_skip');
   variable_del('admin_menu_margin_top');
   variable_del('admin_menu_position_fixed');
   variable_del('admin_menu_tweak_modules');
@@ -44,6 +41,10 @@ function admin_menu_uninstall() {
   variable_del('admin_menu_display');
   variable_del('admin_menu_cache_server');
   variable_del('admin_menu_cache_client');
+  // Unused variables still should be deleted.
+  variable_del('admin_menu_devel_modules');
+  variable_del('admin_menu_devel_modules_enabled');
+  variable_del('admin_menu_devel_modules_skip');
 }
 
 /**

+ 8 - 16
sites/all/modules/contrib/admin/admin_menu/admin_menu.js

@@ -1,4 +1,8 @@
-(function($) {
+/**
+ * @file
+ */
+
+(function ($) {
 
 Drupal.admin = Drupal.admin || {};
 Drupal.admin.behaviors = Drupal.admin.behaviors || {};
@@ -139,7 +143,7 @@ Drupal.admin.getCache = function (hash, onSuccess) {
  *
  * @see toolbar.js
  */
-Drupal.admin.height = function() {
+Drupal.admin.height = function () {
   var $adminMenu = $('#admin-menu');
   var height = $adminMenu.outerHeight();
   // In IE, Shadow filter adds some extra height, so we need to remove it from
@@ -161,7 +165,7 @@ Drupal.admin.height = function() {
 Drupal.admin.attachBehaviors = function (context, settings, $adminMenu) {
   if ($adminMenu.length) {
     $adminMenu.addClass('admin-menu-processed');
-    $.each(Drupal.admin.behaviors, function() {
+    $.each(Drupal.admin.behaviors, function () {
       this(context, settings, $adminMenu);
     });
   }
@@ -206,7 +210,7 @@ Drupal.admin.behaviors.replacements = function (context, settings, $adminMenu) {
  */
 Drupal.admin.behaviors.destination = function (context, settings, $adminMenu) {
   if (settings.admin_menu.destination) {
-    $('a.admin-menu-destination', $adminMenu).each(function() {
+    $('a.admin-menu-destination', $adminMenu).each(function () {
       this.search += (!this.search.length ? '?' : '&') + Drupal.settings.admin_menu.destination;
     });
   }
@@ -219,18 +223,6 @@ Drupal.admin.behaviors.destination = function (context, settings, $adminMenu) {
  *   it will not run last.
  */
 Drupal.admin.behaviors.hover = function (context, settings, $adminMenu) {
-  // Hover emulation for IE 6.
-  if ($.browser.msie && parseInt(jQuery.browser.version) == 6) {
-    $('li', $adminMenu).hover(
-      function () {
-        $(this).addClass('iehover');
-      },
-      function () {
-        $(this).removeClass('iehover');
-      }
-    );
-  }
-
   // Delayed mouseout.
   $('li.expandable', $adminMenu).hover(
     function () {

+ 42 - 52
sites/all/modules/contrib/admin/admin_menu/admin_menu.map.inc

@@ -58,68 +58,59 @@ function node_admin_menu_map() {
 
 /**
  * Implements hook_admin_menu_map() on behalf of Field UI module.
+ *
+ * @todo Figure out how to fix the comment entity bundle mappings.
  */
 function field_ui_admin_menu_map() {
   $map = array();
-  foreach (entity_get_info() as $obj_type => $info) {
-    foreach ($info['bundles'] as $bundle_name => $bundle_info) {
-      if (isset($bundle_info['admin'])) {
-        $arguments = array();
-        switch ($obj_type) {
-          case 'comment':
-            $fields = array();
-            foreach (field_info_instances($obj_type, $bundle_name) as $field) {
-              $fields[] = $field['field_name'];
-            }
-            // @todo Make Comment module expose the original node type bundle,
-            //   pretty please.
-            if (drupal_substr($bundle_name, 0, 13) == 'comment_node_') {
-              $bundle_name = drupal_substr($bundle_name, 13);
-            }
-            // @todo Doesn't work yet. Why?
-            $arguments = array(
-              '%comment_node_type' => array($bundle_name),
-              '%field_ui_menu' => $fields,
-            );
-            break;
 
-          case 'node':
-            $fields = array();
-            foreach (field_info_instances($obj_type, $bundle_name) as $field) {
-              $fields[] = $field['field_name'];
-            }
-            $arguments = array(
-              '%node_type' => array($bundle_name),
-              '%field_ui_menu' => $fields,
-            );
-            break;
+  foreach (entity_get_info() as $entity_type => $entity_info) {
+    if (!$entity_info['fieldable']) {
+      continue;
+    }
 
-          case 'taxonomy_term':
-            $fields = array();
-            foreach (field_info_instances($obj_type, $bundle_name) as $field) {
-              $fields[] = $field['field_name'];
-            }
-            // Map machine_name to vid.
-            $arguments = array(
-              '%taxonomy_vocabulary_machine_name' => array($bundle_name),
-              '%field_ui_menu' => $fields,
-            );
-            break;
+    foreach ($entity_info['bundles'] as $bundle => $bundle_info) {
+      // @see field_ui_menu()
+      if (!isset($bundle_info['admin'])) {
+        continue;
+      }
+
+      // Check access to this bundle.
+      $bundle_info['admin'] += array(
+        'access callback' => 'user_access',
+        'access arguments' => array('administer site configuration'),
+      );
+      $access_arguments = $bundle_info['admin']['access arguments'];
+      if (isset($bundle_info['admin']['real path'])) {
+        $menu_item = menu_get_item($bundle_info['admin']['real path']);
+        if (isset($menu_item['map'])) {
+          $access_arguments = menu_unserialize(serialize($access_arguments), $menu_item['map']);
+        }
+      }
+      if (!call_user_func_array($bundle_info['admin']['access callback'], $access_arguments)) {
+        continue;
+      }
 
-          case 'user':
-            $arguments = array(
-              '%field_ui_menu' => array_keys(field_info_fields('user')),
-            );
-            break;
+      if ($fields = field_info_instances($entity_type, $bundle)) {
+        $path = $bundle_info['admin']['path'];
+        $argument = array();
+        if (isset($bundle_info['admin']['bundle argument'])) {
+          $bundle_arg = arg($bundle_info['admin']['bundle argument'], $path);
+          $argument[$bundle_arg] = array($bundle);
         }
-        if (!empty($arguments)) {
-          $path = $bundle_info['admin']['path'];
-          $map["$path/fields/%field_ui_menu"]['parent'] = "$path/fields";
-          $map["$path/fields/%field_ui_menu"]['arguments'][] = $arguments;
+        $argument['%field_ui_menu'] = array_keys($fields);
+
+        if (!isset($map["$path/fields/%field_ui_menu"])) {
+          $map["$path/fields/%field_ui_menu"] = array(
+            'parent' => "$path/fields",
+            'arguments' => array(),
+          );
         }
+        $map["$path/fields/%field_ui_menu"]['arguments'][] = $argument;
       }
     }
   }
+
   return $map;
 }
 
@@ -161,4 +152,3 @@ function views_ui_admin_menu_map() {
   );
   return $map;
 }
-

+ 25 - 20
sites/all/modules/contrib/admin/admin_menu/admin_menu.module

@@ -63,7 +63,7 @@ function admin_menu_theme() {
 function admin_menu_menu() {
   // AJAX callback.
   // @see http://drupal.org/project/js
-  $items['js/admin_menu/cache'] = array(
+  $items['js/admin_menu/cache/%'] = array(
     'page callback' => 'admin_menu_js_cache',
     'delivery callback' => 'admin_menu_deliver',
     'access arguments' => array('access administration menu'),
@@ -78,7 +78,7 @@ function admin_menu_menu() {
     'file' => 'system.admin.inc',
     'file path' => drupal_get_path('module', 'system'),
   );
-  $items['admin/config/administration/admin_menu'] = array(
+  $items['admin/config/administration/admin-menu'] = array(
     'title' => 'Administration menu',
     'description' => 'Adjust administration menu settings.',
     'page callback' => 'drupal_get_form',
@@ -87,12 +87,6 @@ function admin_menu_menu() {
     'file' => 'admin_menu.inc',
   );
   // Menu link callbacks.
-  $items['admin_menu/toggle-modules'] = array(
-    'page callback' => 'admin_menu_toggle_modules',
-    'access arguments' => array('administer modules'),
-    'type' => MENU_CALLBACK,
-    'file' => 'admin_menu.inc',
-  );
   $items['admin_menu/flush-cache'] = array(
     'page callback' => 'admin_menu_flush_cache',
     'access arguments' => array('flush caches'),
@@ -217,7 +211,7 @@ function admin_menu_page_build(&$page) {
     // @todo Drupal.behaviors.adminMenuMarginTop is obsolete, but
     //   hook_page_build() does not allow to set a CSS class on the body yet.
     // @see http://drupal.org/node/1473548, http://drupal.org/node/1194528
-    //$page['#attributes']['class'][] = 'admin-menu';
+    // $page['#attributes']['class'][] = 'admin-menu';
   }
   if ($setting = variable_get('admin_menu_position_fixed', 1)) {
     $settings['position_fixed'] = $setting;
@@ -236,7 +230,7 @@ function admin_menu_page_build(&$page) {
   if ($_GET['q'] == 'admin/modules' || strpos($_GET['q'], 'admin/modules/list') === 0) {
     $settings['tweak_modules'] = variable_get('admin_menu_tweak_modules', 0);
   }
-  if ($_GET['q'] == 'admin/people/permissions' || $_GET['q'] == 'admin/people/permissions/list') {
+  if (strpos($_GET['q'], 'admin/people/permissions') === 0) {
     $settings['tweak_permissions'] = variable_get('admin_menu_tweak_permissions', 0);
   }
 
@@ -508,7 +502,6 @@ function admin_menu_output($complete = FALSE) {
 
     // @todo Move the below callbacks into hook_admin_menu_build()
     //   implementations (and $module.admin_menu.inc).
-
     // Add administration menu.
     if (!empty($components['admin_menu.menu']) || $complete) {
       $content['menu'] = admin_menu_links_menu(admin_menu_tree('management'));
@@ -554,7 +547,10 @@ function admin_menu_output($complete = FALSE) {
 
   // Store the new hash for this user.
   if (!empty($_COOKIE['has_js']) && !$complete) {
-    admin_menu_cache_set($cid, md5($content));
+    $cache = cache_get($cid, 'cache_admin_menu');
+    if (!$cache || !isset($cache->data)) {
+      admin_menu_cache_set($cid, md5($content));
+    }
   }
 
   return $content;
@@ -609,11 +605,13 @@ function admin_menu_admin_menu_output_build(&$content) {
  * Implements hook_admin_menu_output_alter().
  */
 function admin_menu_admin_menu_output_alter(&$content) {
-  foreach ($content['menu'] as $key => $link) {
-    // Move local tasks on 'admin' into icon menu.
-    if ($key == 'admin/tasks' || $key == 'admin/index') {
-      $content['icon']['icon'][$key] = $link;
-      unset($content['menu'][$key]);
+  if (!empty($content['menu'])) {
+    foreach ($content['menu'] as $key => $link) {
+      // Move local tasks on 'admin' into icon menu.
+      if ($key == 'admin/tasks' || $key == 'admin/index') {
+        $content['icon']['icon'][$key] = $link;
+        unset($content['menu'][$key]);
+      }
     }
   }
 }
@@ -683,6 +681,13 @@ function theme_admin_menu_links($variables) {
         $elements[$path]['#options']['attributes']['class'][] = 'admin-menu-destination';
       }
 
+      // If the path has an alias replace the href with the alias.
+      if (module_exists('path')) {
+        if ($alias = drupal_get_path_alias($elements[$path]['#href'])) {
+          $elements[$path]['#href'] = $alias;
+        }
+      }
+
       $link = l($elements[$path]['#title'], $elements[$path]['#href'], $elements[$path]['#options']);
     }
     // Handle plain text items, but do not interfere with menu additions.
@@ -757,7 +762,7 @@ function admin_menu_translated_menu_link_alter(&$item, $map) {
     }
   }
 
-  // Don't waste cycles altering items that are not visible
+  // Don't waste cycles altering items that are not visible.
   if (!$item['access']) {
     return;
   }
@@ -801,8 +806,8 @@ function admin_menu_flush_caches($uid = NULL) {
   cache_clear_all($cid, 'cache_menu', TRUE);
   // Flush client-side cache hashes.
   drupal_static_reset('admin_menu_cache_get');
-  // db_table_exists() required for SimpleTest.
-  if (db_table_exists('cache_admin_menu')) {
+  // If cache_admin_menu is not empty, flush it.
+  if (!cache_is_empty('cache_admin_menu')) {
     cache_clear_all(isset($uid) ? $cid : '*', 'cache_admin_menu', TRUE);
   }
 }

+ 0 - 1
sites/all/modules/contrib/admin/admin_menu/admin_menu.uid1.css

@@ -1,4 +1,3 @@
-
 /**
  * @file
  * Administration menu color override for uid1.

+ 4 - 0
sites/all/modules/contrib/admin/admin_menu/admin_menu_toolbar/admin_menu_toolbar-rtl.css

@@ -0,0 +1,4 @@
+#admin-menu > div > .dropdown > li > a,
+#admin-menu > div > .dropdown > li > span {
+  border-left: 0;
+}

+ 3 - 4
sites/all/modules/contrib/admin/admin_menu/admin_menu_toolbar/admin_menu_toolbar.css

@@ -1,4 +1,3 @@
-
 /**
  * @file
  * Toolbar style for Administration menu.
@@ -63,8 +62,9 @@ body div#toolbar.toolbar {
   background: url(toolbar.png) no-repeat 0 -45px;
   text-indent: -9999px;
 }
-#admin-menu > div > .dropdown > li > a {
-  border-right: 0;
+#admin-menu > div > .dropdown > li > a,
+#admin-menu > div > .dropdown > li > span {
+  border-right: 0; /* LTR */
   margin-bottom: 4px;
   padding: 2px 10px 3px;
 }
@@ -142,4 +142,3 @@ body div#toolbar.toolbar {
 #admin-menu .shortcut-toolbar a {
   display: block;
 }
-

+ 3 - 4
sites/all/modules/contrib/admin/admin_menu/admin_menu_toolbar/admin_menu_toolbar.info

@@ -4,9 +4,8 @@ package = Administration
 core = 7.x
 dependencies[] = admin_menu
 
-; Information added by drupal.org packaging script on 2013-01-31
-version = "7.x-3.0-rc4"
+; Information added by Drupal.org packaging script on 2018-12-03
+version = "7.x-3.0-rc6"
 core = "7.x"
 project = "admin_menu"
-datestamp = "1359651687"
-
+datestamp = "1543859284"

+ 0 - 1
sites/all/modules/contrib/admin/admin_menu/admin_menu_toolbar/admin_menu_toolbar.install

@@ -34,4 +34,3 @@ function admin_menu_toolbar_update_6300() {
     ->condition('name', 'admin_menu_toolbar')
     ->execute();
 }
-

+ 1 - 2
sites/all/modules/contrib/admin/admin_menu/admin_menu_toolbar/admin_menu_toolbar.module

@@ -70,7 +70,7 @@ function admin_menu_toolbar_admin_menu_output_build(&$content) {
     '#title' => t('Show shortcuts'),
     '#href' => '',
     '#options' => array(
-      'attributes' => array('class' => 'shortcut-toggle'),
+      'attributes' => array('class' => array('shortcut-toggle')),
     ),
   );
 
@@ -115,4 +115,3 @@ function admin_menu_toolbar_admin_menu_output_alter(&$content) {
     $content['account']['account']['#options']['html'] = TRUE;
   }
 }
-

+ 51 - 16
sites/all/modules/contrib/admin/admin_menu/tests/admin_menu.test

@@ -16,7 +16,10 @@ class AdminMenuWebTestCase extends DrupalWebTestCase {
     'admin_menu' => 'access administration menu',
   );
 
-  function setUp() {
+  /**
+   *
+   */
+  public function setUp() {
     // Enable admin menu module and any other modules.
     $modules = func_get_args();
     $modules = isset($modules[0]) ? $modules[0] : $modules;
@@ -108,12 +111,17 @@ class AdminMenuWebTestCase extends DrupalWebTestCase {
     $xpath = '//div[@id="admin-menu"]/div/ul' . implode('/parent::li/ul', $xpath);
     $this->assertNoElementByXPath($xpath, $args, $message . ' link not found.');
   }
+
 }
 
 /**
  * Tests menu links depending on user permissions.
  */
 class AdminMenuPermissionsTestCase extends AdminMenuWebTestCase {
+
+  /**
+   *
+   */
   public static function getInfo() {
     return array(
       'name' => 'Menu link access permissions',
@@ -122,14 +130,17 @@ class AdminMenuPermissionsTestCase extends AdminMenuWebTestCase {
     );
   }
 
-  function setUp() {
+  /**
+   *
+   */
+  public function setUp() {
     parent::setUp(array('node'));
   }
 
   /**
    * Test that the links are added to the page (no JS testing).
    */
-  function testPermissions() {
+  public function testPermissions() {
     module_enable(array('contact'));
     $this->resetAll();
 
@@ -140,7 +151,7 @@ class AdminMenuPermissionsTestCase extends AdminMenuWebTestCase {
     // Create a user who
     // - can access content overview
     // - cannot access drupal.org links
-    // - cannot administer Contact module
+    // - cannot administer Contact module.
     $permissions = $this->basePermissions + array(
       'access content overview',
     );
@@ -156,7 +167,7 @@ class AdminMenuPermissionsTestCase extends AdminMenuWebTestCase {
     // Create a user "reversed" to the above; i.e., who
     // - cannot access content overview
     // - can access drupal.org links
-    // - can administer Contact module
+    // - can administer Contact module.
     $permissions = $this->basePermissions + array(
       'display drupal links',
       'administer contact forms',
@@ -172,7 +183,7 @@ class AdminMenuPermissionsTestCase extends AdminMenuWebTestCase {
   /**
    * Tests handling of links pointing to category/overview pages.
    */
-  function testCategories() {
+  public function testCategories() {
     // Create a user with minimum permissions.
     $admin_user = $this->drupalCreateUser($this->basePermissions);
     $this->drupalLogin($admin_user);
@@ -201,7 +212,7 @@ class AdminMenuPermissionsTestCase extends AdminMenuWebTestCase {
   /**
    * Tests that user role and permission changes are properly taken up.
    */
-  function testPermissionChanges() {
+  public function testPermissionChanges() {
     // Create a user who is able to change permissions.
     $permissions = $this->basePermissions + array(
       'administer permissions',
@@ -253,12 +264,17 @@ class AdminMenuPermissionsTestCase extends AdminMenuWebTestCase {
     // Verify that Structure » Content types does not appear.
     $this->assertNoLinkTrailByTitle(array(t('Structure'), t('Content types')));
   }
+
 }
 
 /**
  * Tests appearance, localization, and escaping of dynamic links.
  */
 class AdminMenuDynamicLinksTestCase extends AdminMenuWebTestCase {
+
+  /**
+   *
+   */
   public static function getInfo() {
     return array(
       'name' => 'Dynamic links',
@@ -267,14 +283,17 @@ class AdminMenuDynamicLinksTestCase extends AdminMenuWebTestCase {
     );
   }
 
-  function setUp() {
+  /**
+   *
+   */
+  public function setUp() {
     parent::setUp(array('node'));
   }
 
   /**
    * Tests node type links.
    */
-  function testNode() {
+  public function testNode() {
     $type = $this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article'));
     // Create a content-type with special characters.
     $type = $this->drupalCreateContentType(array('type' => 'special', 'name' => 'Cool & Special'));
@@ -324,7 +343,7 @@ class AdminMenuDynamicLinksTestCase extends AdminMenuWebTestCase {
   /**
    * Tests Add content links.
    */
-  function testNodeAdd() {
+  public function testNodeAdd() {
     $type = $this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article'));
 
     // Verify that "Add content" does not appear for unprivileged users.
@@ -359,12 +378,17 @@ class AdminMenuDynamicLinksTestCase extends AdminMenuWebTestCase {
       t('Add content'),
     ));
   }
+
 }
 
 /**
  * Tests appearance of different types of links.
  */
 class AdminMenuLinkTypesTestCase extends AdminMenuWebTestCase {
+
+  /**
+   *
+   */
   public static function getInfo() {
     return array(
       'name' => 'Link types',
@@ -373,7 +397,10 @@ class AdminMenuLinkTypesTestCase extends AdminMenuWebTestCase {
     );
   }
 
-  function setUp() {
+  /**
+   *
+   */
+  public function setUp() {
     parent::setUp(array('help'));
 
     $permissions = module_invoke_all('permission');
@@ -385,7 +412,7 @@ class AdminMenuLinkTypesTestCase extends AdminMenuWebTestCase {
   /**
    * Tests appearance of different router item link types.
    */
-  function testLinkTypes() {
+  public function testLinkTypes() {
     // Verify that MENU_NORMAL_ITEMs appear.
     $this->assertLinkTrailByTitle(array(
       t('Configuration'),
@@ -420,12 +447,17 @@ class AdminMenuLinkTypesTestCase extends AdminMenuWebTestCase {
       ':title' => t('Index'),
     ), "Icon » Index link found.");
   }
+
 }
 
 /**
  * Tests customized menu links.
  */
 class AdminMenuCustomizedTestCase extends AdminMenuWebTestCase {
+
+  /**
+   *
+   */
   public static function getInfo() {
     return array(
       'name' => 'Customized links',
@@ -434,7 +466,10 @@ class AdminMenuCustomizedTestCase extends AdminMenuWebTestCase {
     );
   }
 
-  function setUp() {
+  /**
+   *
+   */
+  public function setUp() {
     parent::setUp(array('menu'));
 
     $this->admin_user = $this->drupalCreateUser($this->basePermissions + array(
@@ -446,7 +481,7 @@ class AdminMenuCustomizedTestCase extends AdminMenuWebTestCase {
   /**
    * Test disabled custom links.
    */
-  function testCustomDisabled() {
+  public function testCustomDisabled() {
     $type = $this->drupalCreateContentType();
     $node = $this->drupalCreateNode(array('type' => $type->type));
     $text = $this->randomName();
@@ -488,7 +523,7 @@ class AdminMenuCustomizedTestCase extends AdminMenuWebTestCase {
   /**
    * Tests external links.
    */
-  function testCustomExternal() {
+  public function testCustomExternal() {
     // Add a custom link to the node to the menu.
     $edit = array(
       'link_path' => 'http://example.com',
@@ -516,5 +551,5 @@ class AdminMenuCustomizedTestCase extends AdminMenuWebTestCase {
       ':path' => $path,
     ))->fetchField();
   }
-}
 
+}

+ 333 - 268
sites/all/modules/contrib/admin/admin_theme/LICENSE.txt

@@ -1,274 +1,339 @@
-GNU GENERAL PUBLIC LICENSE
-
-              Version 2, June 1991
-
-Copyright (C) 1989, 1991 Free Software Foundation, Inc. 675 Mass Ave,
-Cambridge, MA 02139, 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 Library 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.
+                    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.)
+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
+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
+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.

+ 21 - 9
sites/all/modules/contrib/admin/admin_theme/README.txt

@@ -1,18 +1,23 @@
-/* $Id: README.txt,v 1.1.2.2.2.1 2008/12/13 09:22:36 davyvandenbremt Exp $ */
 
 Description
 -----------
 
-Drupal allows you to define a different theme for administration pages (Administer -> Site configuration -> Administration theme). By default this only applies to pages with a path starting with 'admin' and content editing pages.
+Drupal allows you to define a different theme for administration pages 
+(Administer -> Site configuration -> Administration theme). 
+By default this only applies to pages with a path starting with 'admin' 
+and content editing pages.
 
-The Administration theme module adds a few more option to the default configuration page like :
+The Administration theme module adds a few more option to 
+the default configuration page like :
 - Use administration theme for batch processing
 - Use administration theme for code reviews
 - ...
 
-Some of these pages will only appear if they apply to your installation, i.e. you have the module installed which generates these pages.
+Some of these pages will only appear if they apply to your installation, 
+i.e. you have the module installed which generates these pages.
 
-You also have the option to define a custom set of Drupal paths or aliases to apply the administration theme for.
+You also have the option to define a custom set of Drupal paths 
+or aliases to apply the administration theme for.
 
 
 Requirements
@@ -41,12 +46,19 @@ Administration theme can be configured at :
   
 Developers
 ----------
-You can add define extra pages where the administration theme should be applied to by implementing the hook_admin_theme_info and hook_admin_theme_check hooks in your modules.
-The first one gets all "options" and the second one checks if each of those options should should be applied to a path. Check out admin_theme_admin_theme_info and admin-theme_admin_theme_check for an example implementation.
+You can add define extra pages where the administration theme 
+should be applied to by implementing the hook_admin_theme_info 
+and hook_admin_theme_check hooks in your modules.
+The first one gets all "options" and the second one checks 
+if each of those options should should be applied to a path. 
+Check out admin_theme_admin_theme_info and admin-theme_admin_theme_check 
+for an example implementation.
 
 
-Author
+Authors
 ------
 
 Davy Van Den Bremt <info@davyvandenbremt.be>
-http://www.davyvandenbremt.be
+http://www.davyvandenbremt.be
+
+Joozer Tohfafarosh

+ 9 - 14
sites/all/modules/contrib/admin/admin_theme/admin_theme.api.php

@@ -1,23 +1,17 @@
 <?php
-// $Id: admin_theme.api.php,v 1.1.2.1 2008/12/13 09:47:29 davyvandenbremt Exp $
 
 /**
  * @file
  * Hooks provided by the Administration theme module.
  */
 
-/**
- * @addtogroup hooks
- * @{
- */
-
 /**
  * Add more options to the administration theme settings page.
  *
  * This hook allows modules to add more options to the administration theme 
  * settings page.
  *
- * @return
+ * @return array
  *   A linear array of associative arrays. The keys of the linear array are 
  *   the identifiers for the "options" that will be check for in 
  *   hook_admin_theme_check. The associative arrays have keys:
@@ -65,26 +59,27 @@ function hook_admin_theme_info() {
  * This hook allows modules to check for each option defined in 
  * hook_admin_theme_info if the option is "on".
  *
- * @param
- *   $option. The option to check.
- * @return
+ * @param string $option
+ *   The option to check.
+ *
+ * @return bool
  *   TRUE or FALSE indicating if the administration theme should be used.
  */
 function hook_admin_theme_check($option = NULL) {
   switch ($option) {
     case 'coder':
       return arg(0) == 'coder';
+
     case 'batch':
       return arg(0) == 'batch';
+
     case 'service_attachments':
       return arg(0) == 'node' && arg(2) == 'service_attachments';
+
     case 'webform_results':
       return arg(0) == 'node' && arg(2) == 'webform-results';
+
     case 'statistics':
       return (arg(0) == 'node' || arg(0) == 'user') && arg(2) == 'track';
   }
 }
-
-/**
- * @} End of "addtogroup hooks".
- */

+ 4 - 7
sites/all/modules/contrib/admin/admin_theme/admin_theme.info

@@ -1,14 +1,11 @@
-; $Id: admin_theme.info,v 1.1.6.5 2010/03/27 06:39:43 davyvandenbremt Exp $
 name = Administration theme
 description = Enable the administration theme on more pages than possible with Drupal's default administration page.
 core = 7.x
 package = Administration
-files[] = admin_theme.module
-files[] = admin_theme.install
 configure = admin/appearance
-; Information added by drupal.org packaging script on 2010-12-06
-version = "7.x-1.0"
+
+; Information added by Drupal.org packaging script on 2020-01-17
+version = "7.x-1.1"
 core = "7.x"
 project = "admin_theme"
-datestamp = "1291630844"
-
+datestamp = "1579273090"

+ 14 - 13
sites/all/modules/contrib/admin/admin_theme/admin_theme.install

@@ -1,11 +1,11 @@
 <?php
-// $Id: admin_theme.install,v 1.1.2.2.4.4 2009/10/20 18:25:45 davyvandenbremt Exp $
 
 /**
  * Implementation of hook_install().
  */
 function admin_theme_install() {
-  // updating module weight to 10 to make it run after system (for user access checks etc)
+  // Updating module weight to 10 to make it run after system.
+  // (For user access checks etc.)
   db_query("UPDATE {system} SET weight = 10 WHERE name = 'admin_theme'");
   
   $link = array('!link' => l(t('Administer > Appearance > Administration theme'), 'admin/appearance'));
@@ -16,13 +16,14 @@ function admin_theme_install() {
  * Implementation of hook_uninstall().
  */
 function admin_theme_uninstall() {
-  // cleaning up general admin theme variables
+  // Cleaning up general admin theme variables.
   variable_del('admin_theme_path');
   variable_del('admin_theme_path_disallow');
-  
-  // cleaning up module defined admin theme variables
-  
-  // we need to add admin theme's explicitly because it's not in the list of modules anymore
+
+  // Cleaning up module defined admin theme variables.
+  // We need to add admin theme's explicitly,
+  // because it's not in the list of modules anymore.
+  drupal_load('module', 'admin_theme');
   $admin_theme_options = admin_theme_admin_theme_info();
   $options = array();
   foreach ($admin_theme_options as $option => $info) {
@@ -30,14 +31,14 @@ function admin_theme_uninstall() {
     $info['module'] = 'admin_theme';
     $options[] = $info;
   }
-  
-  // mergin admin theme's options with other module options
-  // @fixme does this work if other modules are also uninstalled at the same time?
+
+  // Merging admin theme's options with other module options.
+  // @fixme Does this work if other modules are also uninstalled simultaneously?
   $list = array_merge(admin_theme_list(), $options);
-  
-  // deleting the module defined variables
+
+  // Deleting the module defined variables.
   foreach ($list as $info) {
     $var = admin_theme_variable_name($info['module'], $info['option']);
     variable_del($var);
   }
-}
+}

+ 45 - 36
sites/all/modules/contrib/admin/admin_theme/admin_theme.module

@@ -1,13 +1,13 @@
 <?php
-// $Id: admin_theme.module,v 1.1.2.8.2.12 2010/12/06 10:10:22 davyvandenbremt Exp $
 
 /**
  * @file
- * Enable the administration theme on more pages then possible with Drupal's default administration page.
+ * Enable the administration theme on more pages,
+ * then possible with Drupal's default administration page.
  */
 
 /**
- * Implementation of hook_perm().
+ * Implements hook_permission().
  */
 function admin_theme_permission() {
   return array(
@@ -21,28 +21,29 @@ function admin_theme_permission() {
 /**
  * Get the variable name for a certain option.
  *
- * @param $module
- *   String. Module that defines this option.
- * @param $params
- *   String. Name of the option.
- * @return
- *   String. Variable name for the option.
+ * @param string $module
+ *   Module that defines this option.
+ * @param string $params
+ *   Name of the option.
+ * 
+ * @return string
+ *   Variable name for the option.
  */
 function admin_theme_variable_name($module, $option) {
-  return 'admin_theme_'. $module .'_'. $option;
+  return 'admin_theme_' . $module . '_' . $option;
 }
 
 /**
  * Get all module defined options.
  *
- * @return
- *   Array. All options.
+ * @return array
+ *   All options.
  */
 function admin_theme_list() {
   $options = array();
   foreach (module_list() as $module) {
     $module_options = module_invoke($module, 'admin_theme_info');
-    if (count($module_options) > 0) {
+    if ($module_options && count($module_options) > 0) {
       foreach ($module_options as $option => $info) {
         $info['option'] = $option;
         $info['module'] = $module;
@@ -54,23 +55,23 @@ function admin_theme_list() {
 }
 
 /**
- * Implementation of hook_form_alter().
+ * Implements hook_form_alter().
  */
 function admin_theme_form_system_themes_admin_form_alter(&$form, $form_state) {
-  // define a fieldset for the page selection
+  // Define a fieldset for the page selection.
   $form['admin_theme']['pages'] = array(
     '#type' => 'fieldset',
     '#title' => t('Pages'),
     '#collapsible' => TRUE,
-    '#description' => t('Choose which pages should be displayed with the administration theme.')
+    '#description' => t('Choose which pages should be displayed with the administration theme.'),
   );
-  
-  // add the content editing option to the pages fieldset and change the title
+
+  // Add the content editing option to the pages fieldset and change the title.
   $form['admin_theme']['pages']['node_admin_theme'] = $form['admin_theme']['node_admin_theme'];
   $form['admin_theme']['pages']['node_admin_theme']['#title'] = t('Content editing');
   unset($form['admin_theme']['node_admin_theme']);
 
-  // add all options as checkboxes to the admin theme settings form
+  // Add all options as checkboxes to the admin theme settings form.
   $list = admin_theme_list();
   foreach ($list as $info) {
     $var = admin_theme_variable_name($info['module'], $info['option']);
@@ -82,7 +83,8 @@ function admin_theme_form_system_themes_admin_form_alter(&$form, $form_state) {
     );
   }
 
-  // allow the user to define a set of pages where the admin theme should or should not be applied to
+  // Allow the user to define a set of pages where the admin theme should,
+  // or should not be applied to.
   $form['admin_theme']['pages']['custom'] = array(
     '#type' => 'fieldset',
     '#title' => t('Custom'),
@@ -112,7 +114,7 @@ function admin_theme_form_system_themes_admin_form_alter(&$form, $form_state) {
  * Process system_themes_form additions submissions.
  */
 function admin_theme_form_system_themes_form_alter_submit($form, &$form_state) {
-  // module options
+  // Module options.
   $list = admin_theme_list();
   foreach ($list as $info) {
     $var = admin_theme_variable_name($info['module'], $info['option']);
@@ -121,36 +123,37 @@ function admin_theme_form_system_themes_form_alter_submit($form, &$form_state) {
     }
   }
   
-  // custom page options
+  // Custom page options.
   variable_set('admin_theme_path', $form_state['values']['admin_theme_path']);
   variable_set('admin_theme_path_disallow', $form_state['values']['admin_theme_path_disallow']);
 }
 
 /**
- * Implementation of hook_custom_theme().
+ * Implements hook_custom_theme().
  */
 function admin_theme_custom_theme() {
   $admin_theme_disallow = FALSE;
   $admin_theme = FALSE;
   
-  // check if some paths are disallow to get the theme
+  // Check if some paths are disallow to get the theme.
   if (trim(variable_get('admin_theme_path_disallow', '')) != '') {
-    // pages that are defined by their normal path
+    // Pages that are defined by their normal path.
     $admin_theme_disallow = drupal_match_path($_GET['q'], variable_get('admin_theme_path_disallow', ''));
     
-    // pages that are defined with their alias
+    // Pages that are defined with their alias.
     $alias = drupal_get_path_alias($_GET['q']);
     if ($alias != $_GET['q']) {
       $admin_theme_disallow = $admin_theme || drupal_match_path($alias, variable_get('admin_theme_path_disallow', ''));
     }
   }
 
-  // we should not show the admin theme if the user has no access or the path is in the disallow list
+  // We should not show the admin theme if the user has no access,
+  // or the path is in the disallow list.
   if (!user_access('access admin theme') || $admin_theme_disallow) {
     return;
   }
   
-  // check if an option is enabled and if it results to TRUE
+  // Check if an option is enabled and if it results to TRUE.
   $list = admin_theme_list();
   foreach ($list as $info) {
     $var = admin_theme_variable_name($info['module'], $info['option']);
@@ -159,12 +162,12 @@ function admin_theme_custom_theme() {
     }
   }
   
-  // some custom defined pages should get admin theme
+  // Some custom defined pages should get admin theme.
   if (trim(variable_get('admin_theme_path', '')) != '') {
-    // pages that are defined by their normal path
+    // Pages that are defined by their normal path.
     $admin_theme = $admin_theme || drupal_match_path($_GET['q'], variable_get('admin_theme_path', ''));
-    
-    // pages that are defined with their alias
+
+    // Pages that are defined with their alias.
     $alias = drupal_get_path_alias($_GET['q']);
     if ($alias != $_GET['q']) {
       $admin_theme = $admin_theme || drupal_match_path($alias, variable_get('admin_theme_path', ''));
@@ -179,14 +182,14 @@ function admin_theme_custom_theme() {
 }
 
 /**
- * Implementation of hook_admin_theme_info().
+ * Implements hook_admin_theme_info().
  */
 function admin_theme_admin_theme_info() {
   $options = array();
   $options['batch'] = array(
     'title' => t('Batch processing'),
     'description' => t('Use the administration theme when executing batch operations.'),
-  );  
+  );
   if (module_exists('img_assist')) {
     $options['img_assist'] = array(
       'title' => t('Image assist'),
@@ -227,23 +230,29 @@ function admin_theme_admin_theme_info() {
 }
 
 /**
- * Implementation of hook_admin_theme_check().
+ * Implements hook_admin_theme_check().
  */
 function admin_theme_admin_theme_check($option = NULL) {
   switch ($option) {
     case 'img_assist':
       return arg(0) == 'img_assist';
+
     case 'coder':
       return arg(0) == 'coder';
+
     case 'devel':
       return arg(0) == 'devel' || (arg(0) == 'node' && arg(2) == 'devel');
+
     case 'batch':
       return arg(0) == 'batch';
+
     case 'service_attachments':
       return arg(0) == 'node' && arg(2) == 'service_attachments';
+
     case 'webform_results':
       return arg(0) == 'node' && arg(2) == 'webform-results';
+
     case 'statistics':
       return (arg(0) == 'node' || arg(0) == 'user') && arg(2) == 'track';
   }
-}
+}

+ 0 - 122
sites/all/modules/contrib/admin/admin_theme/translations/admin_theme.pot

@@ -1,122 +0,0 @@
-# $Id: admin_theme.pot,v 1.1.4.2 2008/12/15 16:08:50 davyvandenbremt Exp $
-#
-# LANGUAGE translation of Drupal (general)
-# Copyright YEAR NAME <EMAIL@ADDRESS>
-# Generated from files:
-#  admin_theme.module,v 1.1.2.9 2008/12/13 09:17:01 davyvandenbremt
-#  admin_theme.install,v 1.1.2.2 2008/12/08 18:37:44 davyvandenbremt
-#  admin_theme.info,v 1.1 2008/11/14 21:34:23 davyvandenbremt
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: PROJECT VERSION\n"
-"POT-Creation-Date: 2008-12-15 16:59+0100\n"
-"PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\n"
-"Last-Translator: NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <EMAIL@ADDRESS>\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=utf-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
-
-#: admin_theme.module:61
-msgid "Theme"
-msgstr ""
-
-#: admin_theme.module:74
-msgid "Pages"
-msgstr ""
-
-#: admin_theme.module:76
-msgid "Choose which pages should be displayed with the administration theme."
-msgstr ""
-
-#: admin_theme.module:81
-msgid "Content editing"
-msgstr ""
-
-#: admin_theme.module:99
-msgid "Custom"
-msgstr ""
-
-#: admin_theme.module:100
-msgid "Enter one page per line as Drupal paths. The '*' character is a wildcard. Example paths are %blog for the blog page and %blog-wildcard for every personal blog. %front is the front page."
-msgstr ""
-
-#: admin_theme.module:107
-msgid "Use administration theme on the following pages."
-msgstr ""
-
-#: admin_theme.module:112
-msgid "Do not use administration theme on the following pages."
-msgstr ""
-
-#: admin_theme.module:113
-msgid "If a path appears here, the administration theme is not shown even if all above options apply."
-msgstr ""
-
-#: admin_theme.module:186
-msgid "Batch processing"
-msgstr ""
-
-#: admin_theme.module:187
-msgid "Use the administration theme when executing batch operations."
-msgstr ""
-
-#: admin_theme.module:191
-msgid "Code reviews"
-msgstr ""
-
-#: admin_theme.module:192
-msgid "Use the administration theme when viewing Coder code reviews."
-msgstr ""
-
-#: admin_theme.module:197
-msgid "Service attachments form on nodes."
-msgstr ""
-
-#: admin_theme.module:198
-msgid "Use the administration theme when viewing service attachments on nodes."
-msgstr ""
-
-#: admin_theme.module:203
-msgid "Webform submissions."
-msgstr ""
-
-#: admin_theme.module:204
-msgid "Use the administration theme when viewing webform submissions."
-msgstr ""
-
-#: admin_theme.module:209
-msgid "Pages defined by the statistics module."
-msgstr ""
-
-#: admin_theme.module:210
-msgid "Use the administration theme when viewing pages of the statistics module."
-msgstr ""
-
-#: admin_theme.module:13
-msgid "access admin theme"
-msgstr ""
-
-#: admin_theme.module:0
-msgid "admin_theme"
-msgstr ""
-
-#: admin_theme.install:8
-msgid "Administer > Site configuration > Administration theme"
-msgstr ""
-
-#: admin_theme.install:9
-msgid "Administration theme module settings are available under !link"
-msgstr ""
-
-#: admin_theme.info:0
-msgid "Administration theme"
-msgstr ""
-
-#: admin_theme.info:0
-msgid "Enable the administration theme on more pages then possible with Drupal's default administration page."
-msgstr ""
-

+ 0 - 122
sites/all/modules/contrib/admin/admin_theme/translations/nl.po

@@ -1,122 +0,0 @@
-# $Id: nl.po,v 1.2.2.2.4.1 2008/12/15 16:08:50 davyvandenbremt Exp $
-#
-# Dutch translation of Drupal (general)
-# Copyright YEAR NAME <EMAIL@ADDRESS>
-# Generated from files:
-#  admin_theme.module,v 1.1.2.9 2008/12/13 09:17:01 davyvandenbremt
-#  admin_theme.install,v 1.1.2.2 2008/12/08 18:37:44 davyvandenbremt
-#  admin_theme.info,v 1.1 2008/11/14 21:34:23 davyvandenbremt
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: Administration Theme\n"
-"POT-Creation-Date: 2008-11-29 10:18+0100\n"
-"PO-Revision-Date: 2008-11-29 10:27+0100\n"
-"Last-Translator: Davy Van Den Bremt <info@davyvandenbremt.be>\n"
-"Language-Team: Davy Van Den Bremt <info@davyvandenbremt.be>\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=utf-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-"X-Poedit-Language: Dutch\n"
-
-#: admin_theme.module:61
-msgid "Theme"
-msgstr "Template"
-
-#: admin_theme.module:74
-msgid "Pages"
-msgstr "Pagina's"
-
-#: admin_theme.module:76
-msgid "Choose which pages should be displayed with the administration theme."
-msgstr "Kies welke pagina's met de beheertemplate getoond moeten worden."
-
-#: admin_theme.module:81
-msgid "Content editing"
-msgstr "Bewerken van inhoud"
-
-#: admin_theme.module:99
-msgid "Custom"
-msgstr "Specifiek"
-
-#: admin_theme.module:100
-msgid "Enter one page per line as Drupal paths. The '*' character is a wildcard. Example paths are %blog for the blog page and %blog-wildcard for every personal blog. %front is the front page."
-msgstr "Geef één pagina per regel op, als Drupal-paden. Het '*'-teken is een jokerteken. Voorbeeldpaden zijn '%blog' voor de blog-pagina en '%blog-wildcard' voor elke persoonlijke blog. '%front' is de voorpagina."
-
-#: admin_theme.module:107
-msgid "Use administration theme on the following pages."
-msgstr "Gebruik de beheertemplate voor de volgende pagina's."
-
-#: admin_theme.module:112
-msgid "Do not use administration theme on the following pages."
-msgstr "Gebruik de beheertemplate niet voor de volgende pagina's."
-
-#: admin_theme.module:113
-msgid "If a path appears here, the administration theme is not shown even if all above options apply."
-msgstr "Wanneer een pad hier vermeld wordt, word de beheertemplate niet getoond, zelfs als bovenstaande opties van toepassing zijn."
-
-#: admin_theme.module:186
-msgid "Batch processing"
-msgstr "Batch verwerking"
-
-#: admin_theme.module:187
-msgid "Use the administration theme when executing batch operations."
-msgstr "Gebruik de beheertemplate tijdens het verwerken van batch handelingen."
-
-#: admin_theme.module:191
-msgid "Code reviews"
-msgstr "Code revisies"
-
-#: admin_theme.module:192
-msgid "Use the administration theme when viewing Coder code reviews."
-msgstr "Gebruik de beheertemplate tijdens het bekijken van Coder's code revisies."
-
-#: admin_theme.module:197
-msgid "Service attachments form on nodes."
-msgstr "Service attachments formulier bij nodes."
-
-#: admin_theme.module:198
-msgid "Use the administration theme when viewing service attachments on nodes."
-msgstr "Gebruik de beheertemplate tijdens het bekijken van het service attachments formulier bij nodes."
-
-#: admin_theme.module:203
-msgid "Webform submissions."
-msgstr "Webformulier inzendingen."
-
-#: admin_theme.module:204
-msgid "Use the administration theme when viewing webform submissions."
-msgstr "Gebruik de beheertemplate tijdens het bekijken van webformulier inzendingen."
-
-#: admin_theme.module:209
-msgid "Pages defined by the statistics module."
-msgstr "Pagina's van de statistics module"
-
-#: admin_theme.module:210
-msgid "Use the administration theme when viewing pages of the statistics module."
-msgstr "Gebruik de beheertemplate tijdens het bekijken van pagina's van de statistics module."
-
-#: admin_theme.module:13
-msgid "access admin theme"
-msgstr "toegang tot beheertemplate"
-
-#: admin_theme.module:0
-msgid "admin_theme"
-msgstr "admin_theme"
-
-#: admin_theme.install:8
-msgid "Administer > Site configuration > Administration theme"
-msgstr "Beheren > Site-instellingen > Beheertemplate"
-
-#: admin_theme.install:9
-msgid "Administration theme module settings are available under !link"
-msgstr "Instellingen voor de Administration theme module zijn beschikbaar onder !link"
-
-#: admin_theme.info:0
-msgid "Administration theme"
-msgstr "Beheertemplate"
-
-#: admin_theme.info:0
-msgid "Enable the administration theme on more pages then possible with Drupal's default administration page."
-msgstr "Gebruik de beheertemplate op meer pagina's dan instelbaar met Drupal's standaard beheer pagina."
-

+ 310 - 0
sites/all/modules/contrib/admin/backup_migrate/CHANGELOG.txt

@@ -0,0 +1,310 @@
+Backup and Migrate 7.x-3.9, 2020-07-15
+--------------------------------------
+#3158379 by DamienMcKenna, monnerat: Fixed compatibility with PHP 5.4.
+
+
+Backup and Migrate 7.x-3.8, 2020-07-10
+--------------------------------------
+#3119038 by solideogloria, gisle: Spelling typo in update scripts.
+#3143362 by DamienMcKenna, solideogloria: Undefined variable $machine_name in
+  install file.
+#3143250 by DamienMcKenna: Use utf8mb4 for MySQL files.
+#3154166 by DamienMcKenna: Remove use of backup_migrate_include() entirely.
+#3129399 by solideogloria, DamienMcKenna: Missing ctools_export_form parameter.
+#3158378 by DamienMcKenna, ron_s: Adjust default performance settings.
+#3158379 by DamienMcKenna: Coding standards improvements.
+
+
+Backup and Migrate 7.x-3.7, 2020-02-28
+--------------------------------------
+#3032453 by DamienMcKenna: Ignore Advanced Aggregator CSS & JS directories.
+#3026187 by DamienMcKenna: Configurations don't load correctly when upgrading
+  from 7.x-2.x to 3.x, clear caches.
+#2606586 by Darren Oh, dmargetts, joelpittet, DamienMcKenna, solideogloria: Fix
+  smart delete not saving files for all periods.
+#3026186 by DamienMcKenna: Menus not rebuilt after upgrading from 7.x-2.x.
+#2507495 by brankoc, DamienMcKenna, nicorac, lorisbel, crantok, jakeru,
+  NickWilde, DD 85, rothlive, Oleg Ko, mgstables, szczesuil, Web.Pergamonteam,
+  Lemontonix, Tranko, aotus, knalstaaf, drugan, lesliewagner1165, Pascal.s,
+  HansKuiters, mato, lgough, Frosty29, sdowney2002, dhalbert, toomanypets, ckng,
+  havanablackcu1, myDrupal2014_846824658246, felixmf: Warning: mail(): Multiple
+  or malformed newlines found in additional_header in mime_mail->send().
+#2135813 by RickJ, rlhawk: Support for the Encrypt module.
+#3033583 by BrankoC: Add CTools as a test dependency.
+#2946311 by DamienMcKenna, k.elizabeth, couturier, jackalope, nelslynn,
+  rrirower, bmango, Cyberflyer, sharonknieper: Too many connections in
+  backup_migrate_source_db->_get_db_connection().
+#3023804 by DamienMcKenna, solideogloria, adam1, jazzitup, MaxMendez, jprj,
+  Louis Delacretaz, eloiv, oliverpolden, drupal_jon, mato, VirtualMitra: Error:
+  Call to undefined function _backup_migrate_temp_files_delete().
+#2663066 by BrankoC, DamienMcKenna, bsains, izmeez: Profile Filter field -
+  hitting 64Kb limit.
+#3023414 by BrankoC, DamienMcKenna, wylbur: Provide settings page for the hidden
+  variables.
+#1042672 by fietserwin, BrankoC: hook_help: do not translate texts if not
+  necessary.
+#3060997 by solideogloria, BrankoC: Improve Performance of Schedule Calculation.
+#3064783 by BrankoC, DamienMcKenna: Uninstalling this module leaves variable
+  backup_migrate_schedule_last_run_* intact.
+#3051821 by DamienMcKenna, BrankoC, solideogloria, wylbur: Remove NodeSquirrel
+  integration.
+#3033583 by BrankoC, DamienMcKenna, m.stenta, RickJ, wylbur: CTools exportables
+  code no longer loading.
+#2507495 by BrankoC, DamienMcKenna: Update script to disable destinations
+  affected by the email delivery change.
+#3046235 by joelpittet: Slow query 'show table status' on menu rebuild.
+#3102427 by izmeez: Readme.txt minor typo and formatting fixes.
+#3102506 by DamienMcKenna, BrankoC: Verify destinations work before listing them
+  in the Quick Backup form; save the file system form before beginning backup
+  tests to ensure the private file system is configured correctly.
+#3102218 by DamienMcKenna, BrankoC, jacob.embree, izmeez, devad, joelpittet,
+  fietserwin, wylbur: Update 7310 Disable e-mail destinations. Emits syntax
+  error for MySQL and MariaDB.
+#3027709 by BrankoC, DamienMcKenna, ultrabob: "A non-numeric value encountered"
+  in backup_migrate_to_bytes().
+#3116733 by DamienMcKenna, fietserwin: PDOException: SQLSTATE[42S02]: Base table
+  or view not found: 1146 Table 'variable' doesn't exist.
+#2885094 by pdcarto, avergara, couturier: CLI option dumps the ENTIRE database,
+  ignores 'nodata_tables' setting.
+
+
+Backup and Migrate 7.x-3.6, 2018-12-15
+--------------------------------------
+#2293601 by KimNyholm, fastturtle: Errors shown because directory handles are
+  not closed when no longer needed.
+#2937023 by DamienMcKenna: Add a test for the basic UI functionality.
+#2945704 by DamienMcKenna: Write tests for each backup filename option.
+#2939277 by DamienMcKenna, baronmunchowsen: File mode change on
+  sources.archivesource.inc.
+#2935403 by DamienMcKenna, jacob.embree: Improve coding standards compliance.
+#2962822 by laryn: Spelling mistake: "frequecy".
+#2917959 by DamienMcKenna: Updated backup_migrate_update_7305() to fix schema
+  problems.
+#2996191 by mwnciau, DamienMcKenna: Special characters in database password
+  causes PHP notifications.
+#2949211 by gturnbull, DamienMcKenna, Alex Bukach, Eli.Stone, joelpittet:
+  Optional memory limit setting to work around out-of-memory errors during
+  backups.
+#2494757 by pbuyle, DamienMcKenna, Ives, Calystod: Allow running a scheduled
+  backup with Drush.
+#2404113 by DamienMcKenna, John Cook, danharper, salmino: Different host for
+  AWS type server.
+#2829492 by axel.rutz, RickJ, DamienMcKenna, devad, Kris77, pietrocap,
+  couturier: Big file backup breaks with "MySQL server has gone away".
+#3001702 by DamienMcKenna: Code cleanup on 7.x-3.x branch.
+#2831470 by snehal.addweb, bfodeke, jigish.addweb: Delete query in
+  hook_uninstall() can potentially remove unintended variables.
+#3004759 by Manthan.addweb, echoz: Notice: Undefined index: exclude_filepaths...
+  visiting Advanced Backup tab.
+#3013355 by jacob.embree: Syntax error in backup_migrate.js.
+#2415421 by ron_s, coredumperror, blake.thompson, John Bickar: Backup and
+  Migrate module and private file system.
+#3002975 by DamienMcKenna: Improve logging around the cron and backup processes.
+#2747197 by RickJ: Network errors writing to Nodesquirrel handled badly.
+#3014594 by DamienMcKenna: backup_migrate_uninstall() doesn't delete all.
+  variables; renamed "backup_migrate_backup_memory_limit" variable to
+  "backup_migrate_memory_limit".
+#3014601 by DamienMcKenna: Re-enable all tests.
+#3014143 by DamienMcKenna: Add master switch to turn off hook_cron.
+#3014603 by DamienMcKenna: Add a CHANGELOG.txt file.
+#3014596 by DamienMcKenna: Require PHP 5.4.
+#2943719 by DamienMcKenna: Reduce memory usage during MySQL database generation.
+#2338841 by catch, dasha_v, DamienMcKenna: Incorrect translated string +
+  menu_rebuild() performance issue.
+#1104012 by DamienMcKenna, axel.rutz, EmanueleQuinto, gisle, jacob.embree, Pere
+  Orga, frenkx: On restore, provide option to drop all tables.
+#2891272 by DamienMcKenna, webservant316: phpsnif 7.1 says Method name
+  \backup_migrate_destination_nodesquirrel::__xmlrpc\" is discouraged; PHP has
+  reserved all method names with a double underscore prefix for future use. 
+#3014597 by DamienMcKenna: Document the advanced backup options.
+#2382541 by sano, DamienMcKenna, couturier, dman, modulist: Updated
+  instructions for installing the S3 library.
+#3016278 by DamienMcKenna: Some debug messages aren't hidden behind
+  backup_migrate_verbose.
+#3016282 by DamienMcKenna: "Cannot add field
+  backup_migrate_profiles.machine_name: field already exists" error.
+#3017042 by DamienMcKenna, freelylw, gisle: Settings form field dependency
+  broken.
+#3018002 by DamienMcKenna: Improve the module's documentation.
+By DamienMcKenna: "server" not "sever" :-)
+#3018853 by RickJ: Temporary files not deleted.
+#2735337 by RickJ: Settings objects only offer Revert, not Delete.
+
+
+Backup and Migrate 7.x-3.5, 2018-02-05
+--------------------------------------
+#2941981 by DamienMcKenna, Rick J, camhoward: Backup files being overwritten
+  because of a change to the append_timestamp setting.
+#2940451 by Darren Oh: Unlimited max_execution_time treated as 0 seconds.
+
+
+Backup and Migrate 7.x-3.4, 2018-01-24
+--------------------------------------
+By DamienMcKenna, pere-orga, ikit-claw, cashwilliams: Make all permissions
+  restricted.
+#1058820 by kbasarab, sobi3ch, gbirch, avatxus: Add option to overwrite file.
+#2498191 by DamienMcKenna: Unable to backup to NodeSquirrel using PHP 5.6.9
+  (on Windows) due to changes in SSL validation rules.
+#2742855 by hanoii: Caching profile objects led to unwanted behaviors because of
+  stale data.
+#2703351 by DamienMcKenna, ultrabob: Redundant methods in
+  backup_migrate_location_remote.
+#2728345 by Rick J: Nodesquirrel destination class doesn't return its name.
+#2728331 by Rick J: Incorrect paging count in saved backup list.
+#2498179 by junaidpv: Use drupal_mail() for system notifications.
+#2920311 by DamienMcKenna: Check backup directory permissions / web
+  accessibility.
+#2912459 by DamienMcKenna: Add an initial test.
+#2880434 by DamienMcKenna, benqwerty: Minor changes to drush help output.
+#2914644 by DamienMcKenna, jacob.embree: List all files which contain classes in
+  the info file so other modules can use the classes.
+#2663928 by DamienMcKenna, fietserwin: public://languages should also be
+  excluded by default.
+#2388347 by DamienMcKenna: Nodequirrel destination class doesn't define $errno,
+  $errstr but assumes they're available.
+#2899586 by juankvillegas, DamienMcKenna: Ensure the page variable is numeric in
+  backup_migrate_nodesquirrel_status_form().
+
+
+Backup and Migrate 7.x-3.3, 2017-10-18
+--------------------------------------
+#2290707 by DamienMcKenna, mitsuroseba, Darren Oh: Exclude the data from the
+  following tables - does not work.
+#2830554 by axel.rutz: Support Ultimate Cron.
+#2728349 by Rick J: Disabled schedules still run under Elysia cron.
+
+
+Backup and Migrate 7.x-3.2, 2017-09-27
+--------------------------------------
+#2702229 by DamienMcKenna: Automatically add all cache tables to the 'nodata'
+  list, exclude simpletest tables.
+#2618516 by rocketeerbkw: Fatal error when restoring "Entire Site".
+#2839264 by wizonesolutions: Error: [] operator not supported for strings in
+  backup_migrate_crud_ui_list_all().
+#2633304 by adamelleston: 'clone' is a reserved keyword introduced in PHP
+  version 5.0 and cannot be invoked as a function.
+#2547065 by snehi: Description for filesource location is inaccurate.
+#2672478 by serverofworld, lklimek: Wrong destination when running multiple
+  schedules using the same profile.
+#1542274 by dale42, LGLC, AjitS: Restore fails with foreign key constraint error
+  (due to incorrect comment detection).
+#2623598 by SylvainM, joelstein: Deprecated: Methods with the same name as their
+  class will not be constructors in a future version of PHP.
+#2513720 by rrfegade: Code Spell errors.
+By ronan: Cleaned up use of manage url generator.
+#2495681 by DamienMcKenna: Secure all endpoint connections to NodeSquirrel
+  destination service.
+#2495677 by DamienMcKenna: Inconsistent use of https for manage.nodesquirrel.com
+  hostname.
+#2277383 by hswong3i: Fix schema mismatch after upgrade 7.x-3.x.
+
+
+Backup and Migrate 7.x-3.1, 2015-05-11
+--------------------------------------
+By ronan: Added pantheon notice and free notice to NodeSquirrel page.
+#2347637: Add a Note to Backup doesn't work with Jquery > 1.7.
+By ronan: Set Nodesquirrel schedule to use smart delete by default.
+#2477503: Error returned, but exit status is 0.
+#2369461: Scheduled Backups stopped working after 7.x-3.0?.
+#2390057: Site doesn't come out of maintenance mode when backing up entire site.
+#2455265: Update Version Number in README.TXT.
+By ronan: Updated NS messaging slightly.
+#2393615: Compression not running (only when backing up entire site).
+#2324949: Update #7303 Failing.
+#2421183: Stream URI - Not ignored (Recognized?) during backups.
+#2378739: "Exclude the data from the following tables" not respected when "Use
+  cli commands" is checked.
+#2307655 Fixed adding of full system path to site archive.
+#2290707 by marvoey Advanced Backup link on quick backup page goes to wrong
+  place.
+#2290707 Exclude the data from the following tables - does not work.
+By ronan: Moved run time check before scheduled backup attempt to fix too-
+  frequent schedule issue.
+#2280743: Files restore duplicating directory structure
+  (sites/default/files/sites/default/files).
+#2276663 Permissions - Access backup files.
+#2283701 by alarcombe. Restore not possible from archive.
+#2286835 by Fernando Vesga. Advanced backup page link error.
+#2287157 by topsitemakers. Links to "Create new destination" and "Create new
+  schedule" are invalid.
+#2287239 by topsitemakers. Minor typo in an error message.
+#2278865 by topsitemakers Undefined index: files in
+  theme_backup_migrate_file_list.
+
+
+Backup and Migrate 7.x-3.0, 2014-05-27
+--------------------------------------
+By ronan: Fixed comment on install hook.
+#2266381 Warning: Invalid argument supplied for foreach() in
+  backup_migrate_schedules_cron() on line 48.
+#1001654 Fixed ctools exportables.
+By ronan: Added nodesquirrel scheduling back. Fixed issue with restore from
+  previous radios. Added function to retrieve the most recent backup from the
+  given destination. This may be used in a drush command.
+By ronan: Removed link to help text when help module is off.
+By ronan: Removed confusing 'Quick Schedule' tab.
+#2225335 Drush commands not working.
+By ronan: Increased machine_name length in ui for compatibility with migrated
+  items.
+By ronan: Fixed a couple of php errors.
+By ronan: Minor wording tweaks.
+By ronan: Added better status for NodeSquirrel.
+By ronan: Removed dpm.
+By ronan: Added dependency to offline message on restore.
+By ronan: Removed stray console.log from the js.
+By ronan: Added most recent saved backups to the restore tab.
+By ronan: Ported MySQL views support.
+By ronan: Fixed some issues saving profiles.
+By ronan: Added a @TODO to remind me to fix saving on the advance backup page.
+#1974740 allow backup of codebase & files to skip unreadable files.
+By ronan: Merge branch 'refs/heads/NodeSquirrel-Language' into 7.x-3.x.
+By ronan: Fixed up some language and links to NodeSquirrel.
+By ronan: Cleaned up directory handling to support more directory structures.
+By ronan: Added some description language for NodeSquirrel.
+#2166813 Files backup fails when PEAR is not installed.
+#2155381 by JulienD Remove useless files[] directive from .info files.
+#1947206 by Les Lim. "No data" tables are exported even when they're in the
+  "Exclude the following tables altogether" list.
+#2031393 A debugging function call left behind.
+#2031777 Wrong function signature on form callback.
+#2065573 wrong default file token.
+#2039951 settings page gone for schedules.
+#2009392 Settings don't save for file or entire site sources.
+#1998788 missing i in NodeSquirrel.
+By ronan: Moved NodeSquirrel tab to match the 2.x branch.
+By ronan: Some d7 style cleanup.
+By ronan: Fixed nodesquirrel destination with new id structure.
+By ronan: Added checkplain to description on listing.
+By ronan: Fixed an issue with multiselect js invading other pages.
+#1564408 Gzip backups are compressed twice when downloaded.
+#1991686 thx morningtime. Undefined property: backup_migrate_schedule::$last_run
+By ronan: Switch to hourly default to align with smart delete default.
+#839254 Switched to using drupal_reapath to allow for stream wrapper schemas.
+#839254 Checking absolute paths within the docroot.
+#839254 check for access restriction ignores custom destination path.
+#1503202 Suhosin refuses sending mail because of to many new lines in mail
+  header (possible attacker).
+#1529174 Fatal Error when database name is longer than 21 characters.
+By ronan: Fixed 'Restore again' link.
+#1974720 error message on attempting to restore manual backup.
+#1974744 warning on creation of new settings profile.
+By ronan: Fixed inconsistent use of 'source' and 'destination'.
+By ronan: Changed destination_id to machine_name to fix issue with default file
+  destinations.
+By ronan: Fixed error noise caused by ftp not connecting.
+By ronan: Fixed error on file delete form.
+By ronan: Fixed fatal error in metadata util when a backup fails.
+#1968210 error message on attempting backup of entire site.
+By ronan: Improved error reporting when a non-existant file (such as a broken
+  symlink) cannot be backed up.
+#1968196 Error message on install in php 5.4.
+By ronan: Fixed checkboxes.
+By ronan: Fixed fatal error with token replace.
+By ronan: Created full upgrade path from 2.x. Fixed source settings.
+By ronan: Fixed db scheme issue. Fixed file source realpath issue.
+By ronan: Switched type to subtype for ctools compatibility.
+By ronan: Added machine_name js and ctools exportables.
+By ronan: Fixed source specific settings in backup.
+By ronan: Fixed issue with edit screen for source settings in profiles.
+By ronan: Fixed export.
+By ronan: Cleaned up more fatal errors. Better handling of translatable titles.

+ 179 - 57
sites/all/modules/contrib/admin/backup_migrate/README.txt

@@ -1,94 +1,216 @@
+Backup and Migrate
+------------------
+This module makes the task of backing up a site's Drupal database, code and
+all uploaded files and of migrating data from one Drupal install to another
+easier.
 
+Database backup files are a list of SQL statements which can be executed with a
+tool such as phpMyAdmin or the command-line mysql client. File and code backup
+files are tarballs which can be restored by extracting them to the desired
+directory.
+
+
+Installation
 -------------------------------------------------------------------------------
-Backup and Migrate 2 for Drupal 7.x
-  by Ronan Dowling, Gorton Studios - ronan (at) gortonstudios (dot) com
--------------------------------------------------------------------------------
+* Put the module in the Drupal modules directory and enable it via
+  admin/modules.
+* Go to admin/people/permissions and grant permission to any roles that need to
+  be able to backup or restore the database.
+* Configure and use the module at admin/config/system/backup_migrate.
 
-DESCRIPTION:
-This module makes the task of backing up your Drupal database and migrating data
-from one Drupal install to another easier. It provides a function to backup the
-entire database to file or download, and to restore from a previous backup. You
-can also schedule the backup operation. Compression of backup files is also
-supported.
+OPTIONAL:
+* To drop all tables before import, expand "Advanced options" panel under the
+  "Restore" and "Saved backups" tabs and tick the option.
+* Enable token.module to allow token replacement in backup file names.
+* To Backup to Amazon S3:
+  - Download the most recent version from:
+    https://github.com/tpyo/amazon-s3-php-class
+  - Or clone it with command:
+    git clone https://github.com/tpyo/amazon-s3-php-class.git s3-php5-curl
+  - Rename the unzipped folder to s3-php5-curl
 
-There are options to exclude the data from certain tables (such as cache or
-search index tables) to increase efficiency by ignoring data that does not need
-to be backed up or migrated.
+The most recent version of the library known to work is 0.5.1.
 
-The backup files are a list of SQL statements which can be executed with a tool
-such as phpMyAdmin or the command-line mysql client.
 
+Advanced settings
 -------------------------------------------------------------------------------
+Several advanced options are available from the Advanced Settings page:
+* admin/config/system/backup_migrate/settings-advanced
 
-INSTALLATION:
-* Put the module in your Drupal modules directory and enable it in 
-  admin/modules. 
-* Go to admin/people/permissions and grant permission to any roles that need to be 
-  able to backup or restore the database.
-* Configure and use the module at admin/config/system/backup_migrate
+These settings should be handled with care, it is recommended to leave them at
+their defaults unless there is a specific need to modify them.
 
-OPTIONAL:
-* Enable token.module to allow token replacement in backup file names.
-* To Backup to Amazon S3:
-    - Download the S3 library from http://undesigned.org.za/2007/10/22/amazon-s3-php-class
-      and place the file 'S3.php' in the includes directory in this module.
-      The stable version (0.4.0 – 20th Jul 2009) works best with Backup and Migrate.
 
-LIGHTTPD USERS:
-Add the following code to your lighttp.conf to secure your backup directories:
+Additional requirements for LigHTTPd
+-------------------------------------------------------------------------------
+Add the following code to the lighttp.conf to secure the backup directories:
   $HTTP["url"] =~ "^/sites/default/files/backup_migrate/" {
-       url.access-deny = ( "" )
+    url.access-deny = ( "" )
   }
-You may need to adjust the path to reflect the actual path to the files.
+It may be necessary to adjust the path to reflect the actual path to the files.
+
 
-IIS 7 USERS:
-Add the following code to your web.config code to secure your backup directories:
+Additional requirements for IIS 7
+-------------------------------------------------------------------------------
+Add the following code to the web.config code to secure the backup
+directories:
 <rule name="postinst-redirect" stopProcessing="true">
    <match url="sites/default/files/backup_migrate" />
    <action type="Rewrite" url=""/>
 </rule>
-You may need to adjust the path to reflect the actual path to the files.
+It may also be necessary to adjust the path to reflect the actual path to the
+files.
 
--------------------------------------------------------------------------------
 
-VERY IMPORTANT SECURITY NOTE:
-Backup files may contain sensitive data and by default, are saved to your web
+VERY IMPORTANT SECURITY NOTE
+-------------------------------------------------------------------------------
+Backup files may contain sensitive data and, by default, are saved to the web
 server in a directory normally accessible by the public. This could lead to a
 very serious security vulnerability. Backup and Migrate attempts to protect
 backup files using a .htaccess file, but this is not guaranteed to work on all
-environments (and is guaranteed to fail on web servers that are not apache). You
-should test to see if your backup files are publicly accessible, and if in doubt
-do not save backups to the server, or use the destinations feature to save to a 
-folder outside of your webroot.
+environments (and is guaranteed to fail on web servers that are not Apache). It
+is imperative to test to see if the site's backup files are publicly
+accessible, and if in doubt do not save backups to the server, or use the
+destinations feature to save to a folder outside of the site's webroot.
 
-OTHER WARNINGS:
-A failed restore can destroy your database and therefore your entire Drupal
+
+Other warnings
+-------------------------------------------------------------------------------
+A failed restore can destroy the database and therefore the entire Drupal
 installation. ALWAYS TEST BACKUP FILES ON A TEST ENVIRONMENT FIRST. If in doubt
 do not use this module.
 
-This module has only been tested with MySQL and does not work with any other dbms. 
-If you have experiences with Postgres or any other dbms and are willing to help 
-test and modify the module to work with it, please contact the developer at 
-ronan (at) gortonstudios (dot) com.
+This module has only been tested with MySQL and does not work with any other
+dbms. Assistance in adding support for other database systems would be
+appreciated, see the issue queue for further details.
 
-Make sure your php timeout is set high enough to complete a backup or restore
+Make sure the PHP timeout is set high enough to complete a backup or restore
 operation. Larger databases require more time. Also, while the module attempts
 to keep memory needs to a minimum, a backup or restore will require
-significantly more memory then most Drupal operations.
+significantly more memory than most Drupal operations.
 
-If your backup file contains the 'sessions' table all other users will be logged
-out after you run a restore. To avoid this, exclude the sessions table when 
-creating your backups. Be aware though that you will need to recreate the 
-sessions table if you use this backup on an empty database.
+If the backup file contains the 'sessions' table all other users will be logged
+out after running a restore. To avoid this, exclude the sessions table when
+creating the backups. Be aware though that this table is still required to run
+Drupal, so it will need to be recreated if the backup is restored onto an empty
+database.
 
 Do not change the file extension of backup files or the restore function will be
-unable to determine the compression type the file and will not function
+unable to determine the compression type of the file and will not function
 correctly.
 
-IF A RESTORE FAILS:
-Don't panic, the restore file should work with phpMyAdmin's import function, or
-with the mysql command line tool. If it does not, then it is likely corrupt; you
-may panic now. MAKE SURE THAT THIS MODULE IS NOT YOUR ONLY FORM OF BACKUP.
+The module's permissions should only be given to trusted users due to the
+inherent security vulnerabilities in allowing people access to a site's database
+and/or files backups.
+
 
+If a restore fails
 -------------------------------------------------------------------------------
+Don't panic!
 
+The restore file should still work with phpMyAdmin's import function or with
+the mysql command line tool.
+
+If the backup does not restore using either a graphical tool or the command line
+tools, then it is likely corrupt; you may panic now.
+
+MAKE SURE THAT THIS MODULE IS NOT THE ONLY FORM OF BACKUP.
+
+
+Known problems and workarounds
+-------------------------------------------------------------------------------
+* If backups fail due to an out-of-memory, try adjusting the memory limit using
+  the "backup_migrate_memory_limit" variable by adding one of these lines
+  to the site's settings.php file:
+
+  // Backup & Migrate: Use 512MB when generating backups.
+  $conf['backup_migrate_memory_limit'] = '512M';
+
+  // Backup & Migrate: Use 1GB when generating backups.
+  $conf['backup_migrate_memory_limit'] = '1G';
+
+* If backups fail due to a PHP timeout error, especially an error saying "MySQL
+  server has gone away", use the "backup_migrate_backup_max_time" variable to
+  adjust the timeout. Before doing this, check to see what PHP's
+  "max_execution_time" is set to, then set the "backup_migrate_backup_max_time"
+  variable to a higher number, e.g. if max_execution_time is 180 (seconds) try
+  setting backup_migrate_backup_max_time to 240 seconds / 4 minutes, or 300
+  seconds / 5 minutes
+
+  // Backup & Migrate: Adjust the PHP timeout to 5 minutes / 300 seconds.
+  $conf['backup_migrate_backup_max_time'] = 300;
+
+* A variable has been added which may help with problems. Setting the variable
+  'backup_migrate_verbose' to TRUE will make the module log additional messages
+  to watchdog as the module performs certain actions.
+
+  // Backup & Migrate: Log extra messages as the module is working.
+  $conf['backup_migrate_verbose'] = TRUE;
+
+* It can be frustrating working from a production database backup on non-prod
+  servers as schduled backups will automatically run via cron the same as they
+  run on production. The custom cron tasks may be disabled using the
+  "backup_migrate_disable_cron" variable. Note: this doesn't prevent people
+  from manually running backups via the UI or from the Drush commands, so it is
+  safe to hardcode to TRUE on all site instances and then hardcode to FALSE on
+  production environments.
+
+  // Backup & Migrate: Don't run backups via cron.
+  $conf['backup_migrate_disable_cron'] = TRUE;
+
+* There are three different variables that control how MySQL data is processed.
+  Should a site have problems with memory limits, it is worth testing these to
+  see which ones work the best.
+
+  - backup_migrate_data_rows_per_query
+    Controls how many records are loaded from the database at once. Defaults to
+    "1000", i.e. 1,000 rows. Note that setting this to a high number can cause
+    problems when exporting large data sets, e.g. cache tables can have huge
+    volumes of data per record.
+
+    // Backup & Migrate: Load 10,000 rows at once.
+    $conf['backup_migrate_data_rows_per_query'] = 10000;
+
+  - backup_migrate_data_rows_per_line
+    Controls how many records are included in a single INSERT statement.
+    Defaults to "30", i.e. 30 records.
+
+    // Backup & Migrate: Combine no more than five records in a single row.
+    $conf['backup_migrate_data_rows_per_line'] = 5;
+
+  - backup_migrate_data_bytes_per_line
+    Controls how much data will be inserted at once using a single INSERT
+    statement. This works with the "backup_migrate_data_rows_per_line" variable
+    to ensure that each INSERT statement doesn't end up being too large.
+    Defaults to "2000", i.e. 2,000 bytes.
+
+    // Backup & Migrate: Limit the output to 1000 bytes at a time.
+    $conf['backup_migrate_data_bytes_per_line'] = 1000;
+
+
+Development notes
+--------------------------------------------------------------------------------
+It is worth noting that some of the tests will fail when ran against nginx,
+which is the default web server for some local development systems. As a result,
+it is recommended to run tests on a server that uses Apache HTTPd Server instead
+of nginx.
+
+
+Credits / contact
+--------------------------------------------------------------------------------
+Currently maintained by Alex Andrascu [1], Damien McKenna [2] and
+Daniel Pickering [3]. All original development up through 2015 was by
+Ronan Dowling [4] with help by Drew Gorton [5].
+
+The best way to contact the authors is to submit an issue, be it a support
+request, a feature request or a bug report, in the project issue queue:
+  https://www.drupal.org/project/issues/backup_migrate
+
+
+References
+--------------------------------------------------------------------------------
+1: https://www.drupal.org/u/alex-andrascu
+2: https://www.drupal.org/u/damienmckenna
+3: https://www.drupal.org/u/ikit-claw
+4: https://www.drupal.org/u/ronan
+5: https://www.drupal.org/u/dgorton

File diff suppressed because it is too large
+ 43 - 0
sites/all/modules/contrib/admin/backup_migrate/backup_migrate.advanced_settings.inc


+ 41 - 10
sites/all/modules/contrib/admin/backup_migrate/backup_migrate.css

@@ -1,5 +1,10 @@
+/**
+ * @file
+ * Custom CSS for the Backup Migrate module.
+ */
+
 .schedule-list-disabled {
-  filter:alpha(opacity=50);
+  filter: alpha(opacity=50);
   -moz-opacity: .50;
   opacity: .50;
 }
@@ -8,7 +13,7 @@
   font-size: 0.85em;
 }
 
-.backup-migrate-tables-checkboxes .form-item label {
+.backup-migrate-tables-checkboxes .form-item {
   width: 15em;
   float: left;
   overflow: hidden;
@@ -16,24 +21,50 @@
   height: 1.75em;
   margin: .25em .25em 0 0;
 }
-.backup-migrate-tables-checkboxes .form-item label.checked {
-  background-color: #eee;
-}
 .backup-migrate-tables-checkboxes .form-item label input {
-  margin-right: .25em;
+  margin-right: .5em;
 }
 
 div.backup-migrate-tables-checkboxes {
-  max-height: 40em;
+  height: 20em;
   overflow: auto;
 }
-div.backup-migrate-description {
+div.backup-migrate-tags,
+div.backup-migrate-description,
+div.backup-migrate-date {
   font-size: smaller;
 }
-
+span.backup-migrate-label {
+  font-weight: bold;
+}
+table.backup-migrate-listing td {
+  vertical-align: top;
+}
 
 div.row-error {
-  color: #FF0000;
+  color: #ff0000;
   font-weight: bold;
 }
 
+.backup-migrate-listing {
+  margin-bottom: 2em;
+}
+.backup-migrate-actions {
+  text-align: right;
+}
+
+html.js #backup-migrate-ui-manual-quick-backup-form fieldset.collapsed,
+.backup-migrate-inline {
+  margin-bottom: 1em;
+}
+
+.backup-migrate-form-dependent {
+  padding-left: 2em;
+  padding-bottom: 1em;
+}
+.backup-migrate-form-dependent .form-item {
+  margin-top: 0;
+}
+.backup-migrate-footer-message {
+  margin-bottom: 1em;
+}

+ 43 - 8
sites/all/modules/contrib/admin/backup_migrate/backup_migrate.info

@@ -1,17 +1,52 @@
 name = Backup and Migrate
-description = "Backup or migrate the Drupal Database quickly and without unnecessary data."
+description = "Backup the Drupal database and files or migrate them to another environment."
 core = 7.x
+configure = admin/config/system/backup_migrate
+
+; Require PHP 5.4 so that some slightly more advanced PHP logic can be used.
+; This is for future usage; removing this will allow the module to continue
+; working fine for now.
+php = 5.4
 
-files[] = backup_migrate.module
-files[] = backup_migrate.install
+; All of the class files used within the module.
+files[] = includes/crud.inc
 files[] = includes/destinations.inc
+files[] = includes/destinations.browser.inc
+files[] = includes/destinations.db.inc
+files[] = includes/destinations.db.mysql.inc
+files[] = includes/destinations.email.inc
+files[] = includes/destinations.file.inc
+files[] = includes/destinations.ftp.inc
+files[] = includes/destinations.s3.inc
+files[] = includes/files.inc
+files[] = includes/filters.inc
+files[] = includes/filters.backup_restore.inc
+files[] = includes/filters.compression.inc
+files[] = includes/filters.encryption.inc
+files[] = includes/filters.statusnotify.inc
+files[] = includes/filters.utils.inc
+files[] = includes/locations.inc
 files[] = includes/profiles.inc
 files[] = includes/schedules.inc
+files[] = includes/sources.inc
+files[] = includes/sources.archivesource.inc
+files[] = includes/sources.db.inc
+files[] = includes/sources.db.mysql.inc
+files[] = includes/sources.filesource.inc
 
-configure = admin/config/system/backup_migrate
-; Information added by drupal.org packaging script on 2013-04-10
-version = "7.x-2.5"
+; Test dependencies.
+test_dependencies[] = ctools
+
+; Test suite.
+files[] = tests/BmTestBase.test
+files[] = tests/BmTestBasics.test
+files[] = tests/BmTestProfiles.test
+files[] = tests/BmTestEmail.test
+files[] = tests/BmTestCtools.test
+files[] = tests/BmTestUpdate7310.test
+
+; Information added by Drupal.org packaging script on 2020-07-15
+version = "7.x-3.9"
 core = "7.x"
 project = "backup_migrate"
-datestamp = "1365564017"
-
+datestamp = "1594807678"

+ 759 - 100
sites/all/modules/contrib/admin/backup_migrate/backup_migrate.install

@@ -1,29 +1,171 @@
 <?php
 
-
 /**
  * @file
  * Install hooks for Backup and Migrate.
  */
 
-
 /**
- * Implementation of hook_requirements().
+ * Implements hook_requirements().
  */
 function backup_migrate_requirements($phase) {
   $requirements = array();
+
+  // Ensure translations don't break during installation.
+  $t = get_t();
+
+  if ($phase == 'runtime') {
+    // Get a list of all destinations, make sure none of them are publicly
+    // accessible.
+    // @todo Expand the API to add methods to specifically check this.
+    require_once dirname(__FILE__) . '/includes/destinations.inc';
+
+    foreach (backup_migrate_get_destinations() as $dest_name => $destination) {
+      if (method_exists($destination, 'get_display_location')) {
+        $dest_path = $destination->get_display_location();
+        if (!empty($dest_path) && file_valid_uri($dest_path)) {
+          $scheme = file_uri_scheme($dest_path);
+          // Support public and private storage and raw server paths.
+          if ($scheme === 'private' || $scheme === 'public' || substr($dest_path, 0, 1) == '/') {
+            // Check if the path exists.
+            $path_exists = file_prepare_directory($dest_path, FILE_CREATE_DIRECTORY);
+            if ($path_exists) {
+              $real_path = drupal_realpath($dest_path);
+              // See if the private path is somewhere inside the main Drupal
+              // directory structure.
+              if (strpos($real_path, DRUPAL_ROOT) === 0) {
+                // Extract the relative path from the Drupal root path, and
+                // then add the base URL, theoretically creating a fully
+                // qualified URL to the storage directory.
+                $url = substr($real_path, strlen(DRUPAL_ROOT) + 1);
+                $url = url($url, array('absolute' => TRUE));
+                $result = drupal_http_request($url);
+
+                // If the HTTP request comes back as a status 200 that means
+                // there is a directory listing of some sort; directory paths
+                // should return a 503 error.
+                if (!empty($result->code) && $result->code == 200) {
+                  // Get the human readable information for this destination.
+                  $dest_spec = $destination->get_list_row();
+
+                  // Display a warning message.
+                  $requirements['bmdest_' . $dest_name] = array(
+                    'severity' => REQUIREMENT_ERROR,
+                    'title' => 'Backup Migrate',
+                    'value' => $t('Backup destination "%dest" is publicly accessible!', array('%dest' => $dest_spec['name'])),
+                    'description' => $t('The backup destination, "%dest", stores its files in the "%path" directory. This directory is publicly available from the web server and urgently needs to be secured! Please see the Drupal manual on <a href="@manual">configuring the private directory path</a> on how to fix this problem.',
+                      array(
+                        '%dest' => $dest_spec['name'],
+                        '%path'  => $real_path,
+                        '@manual' => 'https://www.drupal.org/docs/7/core/modules/file/overview',
+                      )),
+                  );
+                }
+
+                // Check an individual file.
+                else {
+                  $files = scandir($real_path);
+                  if (!empty($files)) {
+                    foreach ($files as $file) {
+                      // Skip the base field pointers.
+                      if ($file == '.' || $file == '..') {
+                        continue;
+                      }
+
+                      $result = drupal_http_request($url . '/' . $file);
+
+                      // If the HTTP request comes back as a status 200 that
+                      // means the file is accessible.
+                      if (!empty($result->code) && $result->code == 200) {
+                        // Get the human readable information for this
+                        // destination.
+                        $dest_spec = $destination->get_list_row();
+
+                        // Display a warning message.
+                        $requirements['bmdest_' . $dest_name] = array(
+                          'severity' => REQUIREMENT_ERROR,
+                          'title' => 'Backup Migrate',
+                          'value' => $t('Files in "%dest" are publicly accessible!', array('%dest' => $dest_spec['name'])),
+                          'description' => $t('The backup destination, "%dest", stores its files in the "%path" directory. These file(s) are publicly available from the web server and urgently need to be secured! Please see the Drupal manual on <a href="@manual">configuring the private directory path</a> on how to fix this problem.',
+                            array(
+                              '%dest' => $dest_spec['name'],
+                              '%path'  => $real_path,
+                              '@manual' => 'https://www.drupal.org/docs/7/core/modules/file/overview',
+                            )),
+                        );
+                      }
+
+                      // Only need to check one file.
+                      break;
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+    // Leave a note if there were no problems.
+    // @todo No point in displaying this until the API has been expanded.
+    // @code
+    // if (empty($requirements)) {
+    //   $requirements['bmdest_' . $dest_name] = array(
+    //     'severity' => REQUIREMENT_INFO,
+    //     'title' => 'Backup Migrate',
+    //     'value' => $t('Backup destinations are safe'),
+    //     'description' => $t('The backup destinations were all checked and none of them were exposing files to the public. This is a good thing.'),
+    //   );
+    // }
+    // @endcode
+
+    if (variable_get('backup_migrate_disable_cron', FALSE)) {
+      $requirements['bm_disable_cron'] = array(
+        'severity' => REQUIREMENT_INFO,
+        'title' => 'Backup Migrate',
+        'value' => $t('Cron tasks are disabled'),
+        'description' => $t('The cron tasks have been disabled, so scheduled backups will not run. See the Backup & Migrate module\'s README.txt file for further details.'),
+      );
+    }
+  }
+
   return $requirements;
 }
 
 /**
- * Implementation of hook_schema().
+ * Implements hook_schema().
  */
 function backup_migrate_schema() {
   $schema['backup_migrate_profiles'] = array(
+    'export' => array(
+      'key' => 'machine_name',
+      'key name' => 'Profile ID',
+      'admin_title' => 'name',
+      'primary key' => 'profile_id',
+      // Exports will be defined as $preset.
+      'identifier' => 'item',
+      // Function hook name.
+      'default hook' => 'exportables_backup_migrate_profiles',
+      'api' => array(
+        'owner' => 'backup_migrate',
+        // Base name for api include files.
+        'api' => 'backup_migrate_exportables',
+        'minimum_version' => 1,
+        'current_version' => 1,
+      ),
+    ),
     'fields' => array(
       'profile_id' => array(
+        'type' => 'serial',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'description' => 'Primary ID field for the table. Not used for anything except internal lookups.',
+        // Do not export database-only keys.
+        'no export' => TRUE,
+      ),
+      'machine_name' => array(
         'type' => 'varchar',
-        'length' => 32,
+        'length' => 255,
         'not null' => TRUE,
         'default' => '0',
         'description' => 'The primary identifier for a profile.',
@@ -32,13 +174,13 @@ function backup_migrate_schema() {
         'description' => 'The name of the profile.',
         'type' => 'varchar',
         'length' => 255,
-        'not null' => TRUE
+        'not null' => TRUE,
       ),
       'filename' => array(
         'description' => 'The name of the profile.',
         'type' => 'varchar',
         'length' => 255,
-        'not null' => TRUE
+        'not null' => TRUE,
       ),
       'append_timestamp' => array(
         'description' => 'Append a timestamp to the filename.',
@@ -46,17 +188,18 @@ function backup_migrate_schema() {
         'size' => 'tiny',
         'unsigned' => TRUE,
         'not null' => TRUE,
-        'default' => 0
+        'default' => 0,
       ),
       'timestamp_format' => array(
         'description' => 'The format of the timestamp.',
         'type' => 'varchar',
         'length' => 14,
-        'not null' => TRUE
+        'not null' => TRUE,
       ),
       'filters' => array(
         'description' => 'The filter settings for the profile.',
         'type' => 'text',
+        'size' => 'medium',
         'not null' => TRUE,
         'serialize' => TRUE,
         'serialized default' => 'a:0:{}',
@@ -65,30 +208,55 @@ function backup_migrate_schema() {
     'primary key' => array('profile_id'),
   );
   $schema['backup_migrate_destinations'] = array(
+    'export' => array(
+      'key' => 'machine_name',
+      'key name' => 'Destination ID',
+      'admin_title' => 'name',
+      'primary key' => 'destination_id',
+      // Exports will be defined as $preset.
+      'identifier' => 'item',
+      // Function hook name.
+      'default hook' => 'exportables_backup_migrate_destinations',
+      'api' => array(
+        'owner' => 'backup_migrate',
+        // Base name for api include files.
+        'api' => 'backup_migrate_exportables',
+        'minimum_version' => 1,
+        'current_version' => 1,
+      ),
+    ),
     'fields' => array(
       'destination_id' => array(
+        'type' => 'serial',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'description' => 'Primary ID field for the table. Not used for anything except internal lookups.',
+        // Do not export database-only keys.
+        'no export' => TRUE,
+      ),
+      'machine_name' => array(
         'type' => 'varchar',
-        'length' => 32,
+        'length' => 255,
         'not null' => TRUE,
         'default' => '0',
-        'description' => 'The primary identifier for a profile.',
+        'description' => 'The primary identifier for a destination.',
       ),
       'name' => array(
-        'description' => 'The name of the profile.',
+        'description' => 'The name of the destination.',
         'type' => 'varchar',
         'length' => 255,
-        'not null' => TRUE
+        'not null' => TRUE,
       ),
-      'type' => array(
+      'subtype' => array(
         'description' => 'The type of the destination.',
         'type' => 'varchar',
         'length' => 32,
-        'not null' => TRUE
+        'not null' => TRUE,
       ),
       'location' => array(
         'description' => 'The the location string of the destination.',
         'type' => 'text',
-        'not null' => TRUE
+        'not null' => TRUE,
       ),
       'settings' => array(
         'description' => 'Other settings for the destination.',
@@ -96,44 +264,138 @@ function backup_migrate_schema() {
         'not null' => TRUE,
         'serialize' => TRUE,
         'serialized default' => 'a:0:{}',
-     ),
+      ),
     ),
     'primary key' => array('destination_id'),
   );
+  $schema['backup_migrate_sources'] = array(
+    'export' => array(
+      'key' => 'machine_name',
+      'key name' => 'Source ID',
+      'admin_title' => 'name',
+      'primary key' => 'source_id',
+      // Exports will be defined as $preset.
+      'identifier' => 'item',
+      // Function hook name.
+      'default hook' => 'exportables_backup_migrate_sources',
+      'api' => array(
+        'owner' => 'backup_migrate',
+        // Base name for api include files.
+        'api' => 'backup_migrate_exportables',
+        'minimum_version' => 1,
+        'current_version' => 1,
+      ),
+    ),
+    'fields' => array(
+      'source_id' => array(
+        'type' => 'serial',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'description' => 'Primary ID field for the table. Not used for anything except internal lookups.',
+        // Do not export database-only keys.
+        'no export' => TRUE,
+      ),
+      'machine_name' => array(
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '0',
+        'description' => 'The primary identifier for a source.',
+      ),
+      'name' => array(
+        'description' => 'The name of the source.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+      ),
+      'subtype' => array(
+        'description' => 'The type of the source.',
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+      ),
+      'location' => array(
+        'description' => 'The the location string of the source.',
+        'type' => 'text',
+        'not null' => TRUE,
+      ),
+      'settings' => array(
+        'description' => 'Other settings for the source.',
+        'type' => 'text',
+        'not null' => TRUE,
+        'serialize' => TRUE,
+        'serialized default' => 'a:0:{}',
+      ),
+    ),
+    'primary key' => array('source_id'),
+  );
+
   $schema['backup_migrate_schedules'] = array(
+    'export' => array(
+      'key' => 'machine_name',
+      'key name' => 'Source ID',
+      'admin_title' => 'name',
+      'primary key' => 'schedule_id',
+      // Exports will be defined as $preset.
+      'identifier' => 'item',
+      // Function hook name.
+      'default hook' => 'exportables_backup_migrate_schedules',
+      'api' => array(
+        'owner' => 'backup_migrate',
+        // Base name for api include files.
+        'api' => 'backup_migrate_exportables',
+        'minimum_version' => 1,
+        'current_version' => 1,
+      ),
+    ),
     'fields' => array(
       'schedule_id' => array(
+        'type' => 'serial',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'description' => 'Primary ID field for the table. Not used for anything except internal lookups.',
+        // Do not export database-only keys.
+        'no export' => TRUE,
+      ),
+      'machine_name' => array(
         'type' => 'varchar',
-        'length' => 32,
+        'length' => 255,
         'not null' => TRUE,
         'default' => '0',
         'description' => 'The primary identifier for a profile.',
       ),
-     'name' => array(
+      'name' => array(
         'description' => 'The name of the profile.',
         'type' => 'varchar',
         'length' => 255,
-        'not null' => TRUE
+        'not null' => TRUE,
       ),
       'source_id' => array(
         'description' => 'The {backup_migrate_destination}.destination_id of the source to backup from.',
         'type' => 'varchar',
-        'length' => 32,
+        'length' => 255,
         'default' => 'db',
-        'not null' => TRUE
+        'not null' => TRUE,
       ),
       'destination_id' => array(
         'type' => 'varchar',
-        'length' => 32,
+        'length' => 255,
         'not null' => TRUE,
         'default' => '0',
         'description' => 'The {backup_migrate_destination}.destination_id of the destination to back up to.',
       ),
-      'profile_id' => array(
+      'copy_destination_id' => array(
         'type' => 'varchar',
         'length' => 32,
         'not null' => TRUE,
         'default' => '0',
+        'description' => 'A second {backup_migrate_destination}.destination_id of the destination to copy the backup to.',
+      ),
+      'profile_id' => array(
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '0',
         'description' => 'The primary identifier for a profile.',
       ),
       'keep' => array(
@@ -154,15 +416,21 @@ function backup_migrate_schema() {
         'size' => 'tiny',
         'unsigned' => TRUE,
         'not null' => TRUE,
-        'default' => 0
+        'default' => 0,
       ),
       'cron' => array(
         'description' => 'Whether the schedule should be run during cron.',
-        'type' => 'int',
-        'size' => 'tiny',
-        'unsigned' => TRUE,
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+        'default' => 'builtin',
+      ),
+      'cron_schedule' => array(
+        'description' => 'The cron schedule to run on.',
+        'type' => 'varchar',
+        'length' => 255,
         'not null' => TRUE,
-        'default' => 0
+        'default' => '0 4 * * *',
       ),
     ),
     'primary key' => array('schedule_id'),
@@ -172,60 +440,43 @@ function backup_migrate_schema() {
 }
 
 /**
- * Implementation of hook_install().
+ * Implements hook_uninstall().
  */
-function backup_migrate_install() {
-  _backup_migrate_setup_database_defaults();
-}
+function backup_migrate_uninstall() {
+  variable_del('backup_migrate_backup_max_time');
+  variable_del('backup_migrate_cleanup_temp_files');
+  variable_del('backup_migrate_cleanup_time');
+  variable_del('backup_migrate_copy_destination_id');
+  variable_del('backup_migrate_data_bytes_per_line');
+  variable_del('backup_migrate_data_rows_per_line');
+  variable_del('backup_migrate_data_rows_per_query');
+  variable_del('backup_migrate_destination_id');
+  variable_del('backup_migrate_disable_cron');
+  variable_del('backup_migrate_max_email_size');
+  variable_del('backup_migrate_memory_limit');
+  variable_del('backup_migrate_profile_id');
+  variable_del('backup_migrate_schedule_buffer');
+  variable_del('backup_migrate_schedule_last_run_');
+  variable_del('backup_migrate_source_id');
+  variable_del('backup_migrate_timeout_buffer');
+  variable_del('backup_migrate_verbose');
+  variable_del('nodesquirrel_endpoint_urls');
+  variable_del('nodesquirrel_schedule');
+  variable_del('nodesquirrel_schedule_enabled');
+  variable_del('nodesquirrel_schedule_source_id');
+  variable_del('nodesquirrel_secret_key');
 
-function _backup_migrate_setup_database_defaults() {
-  if (variable_get("backup_migrate_file_name", NULL)) {
-    require_once DRUPAL_ROOT . '/'. drupal_get_path('module', 'backup_migrate') .'/includes/crud.inc';
-    require_once DRUPAL_ROOT . '/'. drupal_get_path('module', 'backup_migrate') .'/backup_migrate.module';
-    require_once DRUPAL_ROOT . '/'. drupal_get_path('module', 'backup_migrate') .'/includes/profiles.inc';
-    require_once DRUPAL_ROOT . '/'. drupal_get_path('module', 'backup_migrate') .'/includes/files.inc';
-
-    $settings = array(
-      'profile_id' => 'default',
-      'filename' => variable_get("backup_migrate_file_name", _backup_migrate_default_filename()),
-      'append_timestamp' => variable_get("backup_migrate_append_timestamp", FALSE) ? 1 : 0,
-      'timestamp_format' => variable_get("backup_migrate_timestamp_format", 'Y-m-d\TH-i-s'),
-      'filters' => array(
-        'compression' => variable_get("backup_migrate_compression", "none"),
-        'exclude_tables' => variable_get("backup_migrate_exclude_tables", array()),
-        'nodata_tables' => variable_get("backup_migrate_nodata_tables", array()),
-      ),
-      'name' => t('Default Settings'),
-    );
-    $profile = backup_migrate_crud_create_item('profile', $settings);
-    $profile->save();
-    variable_set("backup_migrate_profile_id", 'default');
-
-    // Set up the default schedules.
-    if (variable_get("backup_migrate_schedule_backup_period", 0)) {
-      require_once DRUPAL_ROOT . '/'. drupal_get_path('module', 'backup_migrate') .'/includes/schedules.inc';
-      $schedule = array(
-        'name' => t('Default Schedule'),
-        'profile_id' => $profile->get_id(),
-        'enabled' => 1,
-        'destination_id' => 'scheduled',
-        'period' => array('number' => variable_get("backup_migrate_schedule_backup_period", 0), 'type' => 'hours'),
-        'keep' => variable_get("backup_migrate_schedule_backup_keep", 0),
-      );
-      $schedule = backup_migrate_crud_create_item('schedule', $schedule);
-      $schedule->save();
-    }
+  // Remove the dynamic generated 'last run' variables.
+  $last_ran_schedules = db_select('variable', 'var')
+    ->fields('var', array('name'))
+    ->condition('name', 'backup\_migrate\_schedule\_last\_run\_%', 'LIKE')
+    ->execute()
+    ->fetchAllAssoc('name', PDO::FETCH_ASSOC);
+  foreach ($last_ran_schedules as $key => $schedule) {
+    variable_del($key);
   }
 }
 
-/**
- * Remove variables on uninstall.
- */
-function backup_migrate_uninstall() {
-  db_query("DELETE FROM {variable} WHERE name LIKE 'backup_migrate_%'");
-  cache_clear_all('variables', 'cache');
-}
-
 /**
  * Update from 1.x to 2.x.
  */
@@ -235,7 +486,7 @@ function backup_migrate_update_2000() {
 }
 
 /**
- * Adding filter field for dev release of 2009-01-28
+ * Adding filter field for dev release of 2009-01-28.
  */
 function backup_migrate_update_2001() {
   $ret = array();
@@ -243,11 +494,21 @@ function backup_migrate_update_2001() {
 
   // Add the filters field to the db.
   if (!db_field_exists('backup_migrate_profiles', 'filters')) {
-    db_add_field('backup_migrate_profiles', 'filters', array('description' => t('The filter settings for the profile.'),'type' => 'text', 'not null' => TRUE));
+    db_add_field('backup_migrate_profiles', 'filters', array(
+      'description' => t('The filter settings for the profile.'),
+      'type' => 'text',
+      'not null' => TRUE,
+    ));
   }
-  // Add the source field
+  // Add the source field.
   if (!db_field_exists('backup_migrate_profiles', 'source_id')) {
-    db_add_field('backup_migrate_profiles', 'source_id', array('description' => t('The {backup_migrate_destination}.destination_id of the source to backup from.'), 'type' => 'varchar', 'length' => 32, 'default' => 'db', 'not null' => TRUE));
+    db_add_field('backup_migrate_profiles', 'source_id', array(
+      'description' => t('The {backup_migrate_destination}.destination_id of the source to backup from.'),
+      'type' => 'varchar',
+      'length' => 255,
+      'default' => 'db',
+      'not null' => TRUE,
+    ));
   }
   // Remove the compression field.
   if (db_field_exists('backup_migrate_profiles', 'compression')) {
@@ -257,7 +518,7 @@ function backup_migrate_update_2001() {
 }
 
 /**
- * Clearing the cache because there was a menu structure change in the dev of 2009-05-31
+ * Clear the cache because there was a menu structure change.
  */
 function backup_migrate_update_2002() {
   // Cache should clear automatically. Nothing to do here.
@@ -265,13 +526,13 @@ function backup_migrate_update_2002() {
 }
 
 /**
- * Allowing non-int profile ids in schedules 2009-05-31
+ * Allowing non-int profile ids in schedules 2009-05-31.
  */
 function backup_migrate_update_2003() {
   $ret = array();
   $spec = array(
     'type' => 'varchar',
-    'length' => 32,
+    'length' => 255,
     'not null' => TRUE,
     'default' => '0',
     'description' => 'The primary identifier for a profile.',
@@ -282,13 +543,13 @@ function backup_migrate_update_2003() {
 }
 
 /**
- * Allowing non-int profile ids 2009-07-01
+ * Allowing non-int profile ids 2009-07-01.
  */
 function backup_migrate_update_2004() {
   $ret = array();
   $spec = array(
     'type' => 'varchar',
-    'length' => 32,
+    'length' => 255,
     'not null' => TRUE,
     'default' => '0',
   );
@@ -314,16 +575,16 @@ function backup_migrate_update_2004() {
 }
 
 /**
- * Change the default database id to something friendlier 2009-08-08
+ * Change the default database id to something friendlier 2009-08-08.
  */
 function backup_migrate_update_2005() {
-  require_once './'. drupal_get_path('module', 'backup_migrate') .'/includes/crud.inc';
-  require_once './'. drupal_get_path('module', 'backup_migrate') .'/includes/profiles.inc';
+  require_once './' . drupal_get_path('module', 'backup_migrate') . '/includes/crud.inc';
+  require_once './' . drupal_get_path('module', 'backup_migrate') . '/includes/profiles.inc';
 
   $ret = array();
-  // Change the destination ids of the defined database sources mostly to make using them with drush friendlier.
-
-  // Change the db_url:default id to simply 'db'
+  // Change the destination ids of the defined database sources mostly to make
+  // using them with drush friendlier.
+  // Change the db_url:default id to simply 'db'.
   $ret[] = db_query("UPDATE {backup_migrate_profiles} SET source_id = 'db' WHERE source_id = 'db_url:default'");
   $ret[] = db_query("UPDATE {backup_migrate_schedules} SET destination_id = 'db' WHERE destination_id = 'db_url:default'");
 
@@ -331,9 +592,15 @@ function backup_migrate_update_2005() {
   $ret[] = db_query("UPDATE {backup_migrate_profiles} SET source_id = REPLACE(source_id, 'db_url:', 'db:')");
   $ret[] = db_query("UPDATE {backup_migrate_schedules} SET destination_id = REPLACE(destination_id, 'db_url:', 'db:')");
 
-  // Add the source field to the schedule
+  // Add the source field to the schedule.
   if (!db_field_exists('backup_migrate_schedules', 'source_id')) {
-    db_add_field('backup_migrate_schedules', 'source_id', array('description' => t('The db source to backup from.'), 'type' => 'varchar', 'length' => 32, 'default' => 'db', 'not null' => TRUE));
+    db_add_field('backup_migrate_schedules', 'source_id', array(
+      'description' => t('The db source to backup from.'),
+      'type' => 'varchar',
+      'length' => 255,
+      'default' => 'db',
+      'not null' => TRUE,
+    ));
   }
 
   // Copy source data from profiles to schedules.
@@ -342,7 +609,7 @@ function backup_migrate_update_2005() {
     if (!$schedule['source_id']) {
       $schedule['source_id'] = 'db';
     }
-    $ret[] = db_query("UPDATE {backup_migrate_schedules} SET source_id = '". $schedule['source_id'] ."' WHERE schedule_id = '". $schedule['profile_id'] ."'");
+    $ret[] = db_query("UPDATE {backup_migrate_schedules} SET source_id = '" . $schedule['source_id'] . "' WHERE schedule_id = '" . $schedule['profile_id'] . "'");
   }
 
   if (db_field_exists('backup_migrate_profiles', 'source_id')) {
@@ -382,19 +649,21 @@ function backup_migrate_update_7200() {
   }
 }
 
-
 /**
  * Change the filename field to support 255 characters.
  */
 function backup_migrate_update_7202() {
   $ret = array();
-  db_change_field('backup_migrate_profiles', 'filename', 'filename', array('type' => 'varchar', 'length' => 255, 'not null' => TRUE));
+  db_change_field('backup_migrate_profiles', 'filename', 'filename', array(
+    'type' => 'varchar',
+    'length' => 255,
+    'not null' => TRUE,
+  ));
   return $ret;
 }
 
-
 /**
- * Update the schedule last run times to use variables instead of saving with the schedule.
+ * Schedule last run times to use variables instead of saving with the schedule.
  */
 function backup_migrate_update_7203() {
   $result = db_query('SELECT * FROM {backup_migrate_schedules}', array(), array('fetch' => PDO::FETCH_ASSOC));
@@ -407,3 +676,393 @@ function backup_migrate_update_7203() {
     db_drop_field('backup_migrate_schedules', 'last_run');
   }
 }
+
+/**
+ * Upgrade from Backup & Migrate 7.x-2.x.
+ *
+ * - Uninstall the Backup Migrate Files module if it's installed.
+ * - Uninstall the NodeSquirrel module if it's installed.
+ * - Upgrade the configurations.
+ */
+function backup_migrate_update_7300() {
+  if (module_exists('backup_migrate_files')) {
+    module_disable(array('backup_migrate_files'));
+    $ret[] = array(
+      'success' => TRUE,
+      'query' => 'Disabled the Backup and Migrate Files module',
+    );
+  }
+  if (module_exists('nodesquirrel')) {
+    module_disable(array('nodesquirrel'));
+    $ret[] = array(
+      'success' => TRUE,
+      'query' => 'Disabled the NodeSquirrel module',
+    );
+  }
+
+  // Add sources to the schema.
+  $schema['backup_migrate_sources'] = array(
+    'fields' => array(
+      'source_id' => array(
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+        'default' => '0',
+        'description' => t('The primary identifier for a source.'),
+      ),
+      'name' => array(
+        'description' => t('The name of the source.'),
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+      ),
+      'type' => array(
+        'description' => t('The type of the source.'),
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+      ),
+      'location' => array(
+        'description' => t('The the location string of the source.'),
+        'type' => 'text',
+        'not null' => TRUE,
+      ),
+      'settings' => array(
+        'description' => t('Other settings for the source.'),
+        'type' => 'text',
+        'not null' => TRUE,
+        'serialize' => TRUE,
+        'serialized default' => 'a:0:{}',
+      ),
+    ),
+    'primary key' => array('source_id'),
+  );
+
+  if (!db_table_exists('backup_migrate_sources')) {
+    db_create_table('backup_migrate_sources', $schema['backup_migrate_sources']);
+  }
+
+  // Move custom destinations to sources.
+  $result = db_query("SELECT * FROM {backup_migrate_destinations} WHERE type in ('filesource', 'db')", array(), array('fetch' => PDO::FETCH_ASSOC));
+  foreach ($result as $item) {
+    $item['source_id'] = $item['destination_id'];
+    drupal_write_record('backup_migrate_source', $item);
+  }
+
+  // Change 'destination' settings to 'source' settings.
+  $result = db_query('SELECT * FROM {backup_migrate_profiles}', array(), array('fetch' => PDO::FETCH_ASSOC));
+  foreach ($result as $item) {
+    $item['filters'] = unserialize($item['filters']);
+    $item['filters']['sources'] = $item['filters']['destinations'];
+    unset($item['filters']['destinations']);
+    drupal_write_record('backup_migrate_profiles', $item, array('profile_id'));
+  }
+
+  // Clear the plugins caches.
+  cache_clear_all('*', 'cache', TRUE);
+
+  // Rebuild the menus.
+  variable_set('menu_rebuild_needed', TRUE);
+}
+
+/**
+ * Switch the cron switch to text.
+ */
+function backup_migrate_update_7301() {
+  db_change_field('backup_migrate_schedules', 'cron', 'cron', array(
+    'type' => 'varchar',
+    'length' => 32,
+    'not null' => TRUE,
+    'default' => 'builtin',
+  ));
+  db_add_field('backup_migrate_schedules', 'cron_schedule', array(
+    'description' => 'The cron schedule to run on.',
+    'type' => 'varchar',
+    'length' => 255,
+    'default' => '0 4 * * *',
+    'not null' => TRUE,
+  ));
+}
+
+/**
+ * Add a second destination to schedules.
+ */
+function backup_migrate_update_7302() {
+  db_add_field('backup_migrate_schedules', 'copy_destination_id',
+    array(
+      'type' => 'varchar',
+      'length' => 32,
+      'not null' => TRUE,
+      'default' => '0',
+      'description' => 'A second {backup_migrate_destination}.destination_id of the destination to copy the backup to.',
+    )
+  );
+}
+
+/**
+ * Add a serial id field to all tables to allow them to be ctools exportable.
+ */
+function backup_migrate_update_7303() {
+  foreach (array(
+    'backup_migrate_sources' => 'source_id',
+    'backup_migrate_destinations' => 'destination_id',
+    'backup_migrate_schedules' => 'schedule_id',
+    'backup_migrate_profiles' => 'profile_id',
+  ) as $table => $id) {
+    // Take the primary key status from the machine name so it can be renamed
+    // A serial field must be defined as a key, so make a temporary index.
+    // See: https://www.drupal.org/node/190027
+    db_add_index($table, 'temp', array($id));
+    db_drop_primary_key($table);
+    // Drop our temporary index.
+    db_drop_index($table, 'temp');
+
+    // Switch the name of the id to 'machine_name' to be more ctools standard.
+    db_change_field($table, $id, 'machine_name', array(
+      'type' => 'varchar',
+      'length' => 32,
+      'not null' => TRUE,
+    ));
+
+    // Add a serial ID.
+    db_add_field($table, $id,
+      array(
+        'type' => 'serial',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'description' => 'Primary ID field for the table. Not used for anything except internal lookups.',
+        // Do not export database-only keys.
+        'no export' => TRUE,
+      ),
+        array('primary key' => array($id))
+    );
+  }
+  foreach (array('backup_migrate_sources', 'backup_migrate_destinations') as $table) {
+    db_change_field($table, 'type', 'subtype', array(
+      'type' => 'varchar',
+      'length' => 32,
+      'not null' => TRUE,
+    ));
+  }
+}
+
+/**
+ * Update all schedules to use the built in cron if none is specified.
+ */
+function backup_migrate_update_7304() {
+  db_query("UPDATE {backup_migrate_schedules} SET cron = 'builtin' WHERE cron = '0'");
+}
+
+/**
+ * Fix schema mismatch after upgrade.
+ */
+function backup_migrate_update_7305() {
+  // Fix the 'machine_name' table fields.
+  $field_spec = array(
+    'type' => 'varchar',
+    'length' => 255,
+    'not null' => TRUE,
+    'default' => '0',
+  );
+  foreach (array(
+    'backup_migrate_profiles',
+    'backup_migrate_destinations',
+    'backup_migrate_sources',
+    'backup_migrate_schedules',
+  ) as $table) {
+    if (!db_field_exists($table, 'machine_name')) {
+      try {
+        db_add_field($table, 'machine_name', $field_spec);
+      }
+      catch (\Exception $e) {
+        db_change_field($table, 'machine_name', 'machine_name', $field_spec);
+      }
+    }
+    else {
+      db_change_field($table, 'machine_name', 'machine_name', $field_spec);
+    }
+  }
+
+  // Fix the 'cron' table field.
+  $field_spec = array(
+    'type' => 'varchar',
+    'length' => 32,
+    'not null' => TRUE,
+    'default' => 'builtin',
+  );
+  if (!db_field_exists('backup_migrate_schedules', 'cron')) {
+    try {
+      db_add_field('backup_migrate_schedules', 'cron', $field_spec);
+    }
+    catch (\Exception $e) {
+      db_change_field('backup_migrate_schedules', 'cron', 'cron', $field_spec);
+    }
+  }
+  else {
+    db_change_field('backup_migrate_schedules', 'cron', 'cron', $field_spec);
+  }
+}
+
+/**
+ * Leave a message to explain the mixup over the backup option.
+ */
+function backup_migrate_update_7306() {
+  drupal_set_message(t('Please note that release 7.x-3.4 had a bug which caused all backups to be overwritten instead of having a timestamp added. Please review all backup settings to ensure they work as intended.'), 'warning');
+}
+
+/**
+ * - 'backup_migrate_backup_memory_limit' vs 'backup_migrate_memory_limit'.
+ */
+function backup_migrate_update_7307() {
+  $limit = variable_get('backup_migrate_backup_memory_limit');
+  if (!empty($limit)) {
+    variable_set('backup_migrate_memory_limit', $limit);
+    variable_del('backup_migrate_backup_memory_limit');
+  }
+}
+
+/**
+ * Update profiles table filter field to accommodate larger serialized strings.
+ */
+function backup_migrate_update_7308() {
+  db_change_field('backup_migrate_profiles', 'filters', 'filters', array(
+    'description' => 'The filter settings for the profile.',
+    'type' => 'text',
+    'size' => 'medium',
+    'not null' => TRUE,
+    'serialize' => TRUE,
+    'serialized default' => 'a:0:{}',
+  ));
+}
+
+/**
+ * NodeSquirrel support has been removed.
+ */
+function backup_migrate_update_7309() {
+  require_once dirname(__FILE__) . '/includes/destinations.inc';
+  require_once dirname(__FILE__) . '/includes/schedules.inc';
+
+  foreach (backup_migrate_get_schedules() as $schedule) {
+    // Look for backups which use NodeSquirrel as its destination.
+    if ($schedule->destination_id == 'nodesquirrel') {
+      $name = $schedule->name;
+
+      // If this schedule had a second destination, swap the destinations.
+      if (!empty($schedule->copy_destination_id)) {
+        $destination = backup_migrate_get_destination($schedule->copy_destination_id);
+        $schedule->destination_id = $schedule->copy_destination_id;
+        $schedule->copy_destination_id = '';
+        $schedule->name = $destination->name;
+        $schedule->save();
+        drupal_set_message(t('The backup schedule named "%backup" was renamed and now just backups to %destination.', array('%backup' => $name, '%destination' => $destination->name)));
+      }
+
+      // Just delete it.
+      else {
+        $schedule->delete();
+        drupal_set_message(t('The backup schedule named "%backup" as been deleted.', array('%backup' => $name)));
+      }
+    }
+
+    // Backups which used NodeSquirrel as the second destination will have the
+    // second destination disabled.
+    elseif ($schedule->copy_destination_id == 'nodesquirrel') {
+      $schedule->copy_destination_id = '';
+      $schedule->save();
+      drupal_set_message(t('The backup schedule named "%backup" no longer keeps a second backup on NodeSquirrel.', array('%backup' => $schedule->name)));
+    }
+  }
+
+  // Clear the cache so that the NodeSquirrel plugin is no longer loaded.
+  cache_clear_all('*', 'cache', TRUE);
+
+  // Rebuild the menus so that the NodeSquirrel menu item is removed.
+  variable_set('menu_rebuild_needed', TRUE);
+
+  // @todo Remove the configuration later.
+  drupal_set_message(t('<a href="!url">NodeSquirrel stopped being usable as a backup destination</a> on October 1st, 2019 and ceased operations entirely on November 1st, 2019. Because of this, the NodeSquirrel functionality has been disabled. Please switch to an alternate destination if necessary. Please note that the NodeSquirrel configuration itself has not been removed.', array('!url' => 'https://pantheon.io/nodesquirrel-service-end-life')));
+}
+
+/**
+ * Disable e-mail destinations.
+ */
+function backup_migrate_update_7310() {
+  $out = '';
+
+  // Disable scheduled e-mail back-ups to allow users to review their
+  // destinations first.
+  //
+  // Which e-mail destinations exist?
+  $destinations = db_select('backup_migrate_destinations', 'bmd')
+    ->fields('bmd', array('machine_name'))
+    ->condition('subtype', 'email', '=')
+    ->execute()
+    ->fetchAllAssoc('machine_name', PDO::FETCH_ASSOC);
+  $destinations = array_keys($destinations);
+
+  if (!empty($destinations)) {
+    // Which schedules contain enabled e-mail destinations?
+    $or = db_or();
+    $or->condition('destination_id', $destinations, 'IN');
+    $or->condition('copy_destination_id', $destinations, 'IN');
+    $schedules_query = db_select('backup_migrate_schedules', 'bms')
+      ->fields('bms', array('schedule_id', 'name'))
+      ->condition($or)
+      ->condition('enabled', 1, '=');
+    $schedules = $schedules_query->execute()->fetchAllAssoc('schedule_id', PDO::FETCH_ASSOC);
+
+    // Disable the relevant schedules.
+    if (count($schedules) > 0) {
+      $email_schedules = array_keys($schedules);
+      $names = array_column($schedules, 'name');
+      $replacements = array('@schedules' => '<ul><li>' . implode('</li><li>', $names) . '</li></ul>');
+      db_update('backup_migrate_schedules')
+        ->fields(['enabled' => '0'])
+        ->condition('schedule_id', $email_schedules, 'IN')
+        ->execute();
+      $out .= t('Schedules that back up to e-mail destinations have been disabled. Check that you are using the correct e-mail addresses, then re-enable manually. The following schedules have been disabled: @schedules', $replacements);
+    }
+  }
+
+  if (!empty($out)) {
+    return $out;
+  }
+  else {
+    return t('No destinations were affected by this change.');
+  }
+}
+
+/**
+ * Adjust the default performance settings.
+ */
+function backup_migrate_update_7311() {
+  $settings = array(
+    'backup_migrate_data_rows_per_query' => array(
+      'old' => 1000,
+      'new' => 50000,
+    ),
+    'backup_migrate_data_rows_per_line' => array(
+      'old' => 30,
+      'new' => 10,
+    ),
+    'backup_migrate_data_bytes_per_line' => array(
+      'old' => 2000,
+      'new' => 2 * 1024,
+    ),
+  );
+  foreach ($settings as $var_name => $data) {
+    $stored = variable_get($var_name);
+
+    // Nothing was stored in the database.
+    if (empty($stored)) {
+      // Don't do anything, the new default will be picked up automatically.
+    }
+    // The old value was smaller than or equal to the new value, or was the
+    // same as the old default, delete it so that the new default can be picked
+    // up.
+    elseif (intval($stored) <= $data['new'] || $stored == $data['old']) {
+      variable_del($var_name);
+      drupal_set_message(t('!var setting reset to the new default of !val.', array('!var' => $var_name, '!val' => $data['new'])));
+    }
+  }
+}

+ 118 - 51
sites/all/modules/contrib/admin/backup_migrate/backup_migrate.js

@@ -1,59 +1,126 @@
+/**
+ * @file
+ * Custom JS for the Backup and Migrate module.
+ */
 
 (function ($) {
-Drupal.backup_migrate = {
-  callbackURL : "",  
-  autoAttach  : function() {
-    if (Drupal.settings.backup_migrate !== undefined) {
-      if ($("#edit-save-settings").length && !$("#edit-save-settings").attr("checked")) {
-        // Disable input and hide its description.
-        // Set display none instead of using hide(), because hide() doesn't work when parent is hidden.
-        $('div.backup-migrate-save-options').css('display', 'none');
-      }
-  
-      $("#edit-save-settings").bind("click", function() {
-        if (!$("#edit-save-settings").attr("checked")) {
-          $("div.backup-migrate-save-options").slideUp('slow');
-        }
-        else {
-          // Save unchecked; enable input.
-          $("div.backup-migrate-save-options").slideDown('slow');
-        }
-      });
+  'use strict';
 
-      $('select[multiple]').each(function() {
-          $(this).after(
-            $('<div class="description backup-migrate-checkbox-link"></div>').append(
-              $('<a href="javascript:null;"></a>').text(Drupal.settings.backup_migrate.checkboxLinkText).click(function() {
-                Drupal.backup_migrate.selectToCheckboxes($(this).parents('.form-item').find('select'));
+  Drupal.behaviors.backupMigrate = {
+    attach: function (context, settings) {
+      if (Drupal.settings.backup_migrate !== undefined) {
+        if (Drupal.settings.backup_migrate.dependents !== undefined) {
+          var key;
+          for (key in Drupal.settings.backup_migrate.dependents) {
+            info = Drupal.settings.backup_migrate.dependents[key];
+            var dependent = $('#edit-' + info['dependent']);
+            for (key in info['dependencies']) {
+              $('[name="' + key + '"]').each(function () {
+                var dependentval = info['dependencies'][key];
+                var dependency = $(this);
+                (function (dependent, dependency) {
+                  var checkval = function (inval) {
+                    // Do loose comparisons to support things like "true", "1",
+                    // etc.
+                    if (dependency.attr('type') === 'radio') {
+                      var val = $('[name="' + dependency.attr('name') + '"]:checked').val();
+                      return val == inval;
+                    }
+                    else if (dependency.attr('type') === 'checkbox') {
+                      return dependency.is(':checked') && inval == dependency.val();
+                    }
+                    else {
+                      return dependency.val() == inval;
+                    }
+                    return false;
+                  };
+                  if (!checkval(dependentval)) {
+                    // Hide doesn't work inside collapsed fieldsets.
+                    dependent.css('display', 'none');
+                  }
+                  dependency.bind('load change click keypress focus', function () {
+                    if (checkval(dependentval)) {
+                      dependent.slideDown();
+                    }
+                    else {
+                      dependent.slideUp();
+                    }
+                  }).load();
+                })(dependent, dependency);
+              });
+            }
+          }
+          for (key in Drupal.settings.backup_migrate.destination_selectors) {
+            var info = Drupal.settings.backup_migrate.destination_selectors[key];
+            (function (info) {
+              var selector = $('#' + info['destination_selector']);
+              var copy = $('#' + info['copy'])
+              var copy_selector = $('#' + info['copy_destination_selector']);
+              var copy_selector_options = {};
+
+              // Store a copy of the secondary selector options.
+              copy_selector.find('optgroup').each(function () {
+                var label = $(this).attr('label');
+                copy_selector_options[label] = [];
+                $(this).find('option').each(function () {
+                  copy_selector_options[label].push(this);
+                });
+                $(this).remove();
               })
-            )
-          );
-        }
-      );
-    }
-  },
 
-  selectToCheckboxes : function($select) {
-    var field_id = $select.attr('id');
-    var $checkboxes = $('<div></div>').addClass('backup-migrate-tables-checkboxes');
-    $('option', $select).each(function(i) {
-      var self = this;
-      $box = $('<input type="checkbox" class="backup-migrate-tables-checkbox">').bind('change click', function() {
-        $select.find('option[value="'+self.value+'"]').attr('selected', this.checked);
-        if (this.checked) {
-          $(this).parent().addClass('checked');
-        }
-        else {
-          $(this).parent().removeClass('checked');
+              // Assign an action to the main selector to modify the secondary
+              // selector.
+              selector.each(function () {
+                $(this).bind('load change click keypress focus', function () {
+                  var group = $(this).find('option[value=' + $(this).val() + ']').parents('optgroup').attr('label');
+                  if (group) {
+                    copy.parent().find('.backup-migrate-destination-copy-label').text(info['labels'][group]);
+                    copy_selector.empty();
+                    for (var key in copy_selector_options) {
+                      if (key != group) {
+                        copy_selector.append(copy_selector_options[key]);
+                      }
+                    }
+                  }
+                }).load();
+              });
+            })(info);
+          }
+
+          // Add the convert to checkboxes functionality to all multiselects.
+          $('#backup-migrate-ui-manual-backup-form select[multiple], #backup-migrate-crud-edit-form select[multiple]').each(function () {
+            var self = this;
+            $(self).after(
+              $('<div class="description backup-migrate-checkbox-link"></div>').append(
+                $('<a>' + Drupal.settings.backup_migrate.checkboxLinkText + '</a>').click(function () {
+                  var $select = $(self);
+                  var $checkboxes = $('<div></div>').addClass('backup-migrate-tables-checkboxes');
+                  $('option', $select).each(function (i) {
+                    $checkboxes.append(
+                      $('<div class="form-item"></div>').append(
+                        $('<label class="option backup-migrate-table-select">' + this.value + '</label>').prepend(
+                          $('<input type="checkbox" class="backup-migrate-tables-checkbox" name="' + $select.attr('name') + '"' + (this.selected ? 'checked="checked"' : '') + ' value="' + this.value + '"/>')
+                            .bind('click change load', function () {
+                              if (this.checked) {
+                                $(this).parent().addClass('checked');
+                              }
+                              else {
+                                $(this).parent().removeClass('checked');
+                              }
+                            }).load()
+                        )
+                      )
+                    );
+                  });
+                  $select.parent().find('.backup-migrate-checkbox-link').remove();
+                  $select.before($checkboxes);
+                  $select.hide();
+                })
+              )
+            );
+          });
         }
-      }).attr('checked', this.selected ? 'checked' : '');
-      $checkboxes.append($('<div class="form-item"></div>').append($('<label class="option backup-migrate-table-select">'+this.value+'</label>').prepend($box)));
-    });
-    $select.parent().find('.backup-migrate-checkbox-link').remove();
-    $select.before($checkboxes);
-    $select.hide();
+      }
+    }
   }
-}
-
-$(document).ready(Drupal.backup_migrate.autoAttach);
 })(jQuery);

File diff suppressed because it is too large
+ 15 - 10
sites/all/modules/contrib/admin/backup_migrate/backup_migrate.module


+ 145 - 50
sites/all/modules/contrib/admin/backup_migrate/includes/backup_migrate.drush.inc

@@ -1,13 +1,12 @@
 <?php
 
-
 /**
  * @file
  * Drush commands for backup and migrate.
  */
- 
+
 /**
- * Implementation of hook_drush_command().
+ * Implements hook_drush_command().
  */
 function backup_migrate_drush_command() {
   $items['bam-backup'] = array(
@@ -15,9 +14,9 @@ function backup_migrate_drush_command() {
     'description' => dt('Backup the site\'s database with Backup and Migrate.'),
     'aliases' => array('bb'),
     'examples' => array(
-      'drush bam-backup' => 'Backup the default databse to the manual backup directory using the default settings.', 
-      'drush bam-backup db scheduled mysettings' => 'Backup the database to the scheduled directory using a settings profile called "mysettings"', 
-      'drush bam-backup files' => 'Backup the files directory to the manual directory using the default settings. The Backup and Migrate Files module is required for files backups.', 
+      'drush bam-backup' => 'Backup the default database to the manual backup directory using the default settings.',
+      'drush bam-backup db scheduled mysettings' => 'Backup the database to the scheduled directory using a settings profile called "mysettings"',
+      'drush bam-backup files' => 'Backup the files directory to the manual directory using the default settings.',
     ),
     'arguments' => array(
       'source'        => "Optional. The id of the source (usually a database) to backup. Use 'drush bam-sources' to get a list of sources. Defaults to 'db'",
@@ -41,7 +40,6 @@ function backup_migrate_drush_command() {
     'callback' => 'backup_migrate_drush_destinations',
     'description' => dt('Get a list of available destinations.'),
   );
-
   $items['bam-sources'] = array(
     'callback' => 'backup_migrate_drush_sources',
     'description' => dt('Get a list of available sources.'),
@@ -54,25 +52,40 @@ function backup_migrate_drush_command() {
     'callback' => 'backup_migrate_drush_destination_files',
     'description' => dt('Get a list of previously created backup files.'),
     'arguments' => array(
-      'destination'   => "Required. The id of destination to list backups from. Use 'drush bam-destinations' to get a list of destinations.",
+      'destination'   => "Optional. The id of destination to list backups from. Use 'drush bam-destinations' to get a list of destinations.",
+    ),
+  );
+  $items['bam-schedule'] = array(
+    'callback' => 'backup_migrate_drush_schedule',
+    'description' => dt('Backup using a specific schedule.'),
+    'arguments' => array(
+      'schedule_id' => dt('The ID of the schedule to run.'),
     ),
   );
+  $items['bam-schedules'] = array(
+    'callback' => 'backup_migrate_drush_schedules',
+    'description' => dt('Get a list of available schedules.'),
+  );
   return $items;
 }
 
 /**
- * Implementation of hook_drush_help().
+ * Implements hook_drush_help().
  */
 function backup_migrate_drush_help($section) {
   switch ($section) {
     case 'drush:bam-backup':
       return dt("Backup the site's database using default settings.");
+
     case 'drush:bam-restore':
       return dt('Restore the site\'s database with Backup and Migrate.');
+
     case 'drush:bam-destinations':
       return dt('Get a list of available destinations.');
+
     case 'drush:bam-profiles':
       return dt('Get a list of available settings profiles.');
+
     case 'drush:bam-backups':
       return dt('Get a list of previously created backup files.');
   }
@@ -82,12 +95,14 @@ function backup_migrate_drush_help($section) {
  * Backup the default database.
  */
 function backup_migrate_drush_backup($source_id = 'db', $destination_id = 'manual', $profile_id = 'default') {
-  backup_migrate_include('profiles', 'destinations');
+  require_once dirname(__FILE__) . '/destinations.inc';
+  require_once dirname(__FILE__) . '/profiles.inc';
+  require_once dirname(__FILE__) . '/sources.inc';
 
   // Set the message mode to logging.
   _backup_migrate_message_callback('_backup_migrate_message_drush');
 
-  if (!backup_migrate_get_destination($source_id)) {
+  if (!backup_migrate_get_source($source_id)) {
     _backup_migrate_message("Could not find the source '@source'. Try using 'drush bam-sources' to get a list of available sources or use 'db' to backup the Drupal database.", array('@source' => $source_id), 'error');
     return;
   }
@@ -96,7 +111,7 @@ function backup_migrate_drush_backup($source_id = 'db', $destination_id = 'manua
     return;
   }
   $settings = backup_migrate_get_profile($profile_id);
-  if(!$settings) {
+  if (!$settings) {
     _backup_migrate_message("Could not find the profile '@profile'. Try using 'drush bam-profiles' to get a list of available profiles.", array('@profile' => $profile_id), 'error');
     return;
   }
@@ -108,20 +123,57 @@ function backup_migrate_drush_backup($source_id = 'db', $destination_id = 'manua
 }
 
 /**
- * Restore to the default database.
+ * Backup using schedule.
  */
-function backup_migrate_drush_restore($source_id = '', $destination_id = '', $file_id = '') {
-  drush_print(dt('Restoring will delete some or all of your data and cannot be undone. ALWAYS TEST YOUR BACKUPS ON A NON-PRODUCTION SERVER!'));
-  if (!drush_confirm(dt('Are you sure you want to restore the database?'))) {
-    return drush_user_abort();
+function backup_migrate_drush_schedule($schedule_id = '') {
+  require_once dirname(__FILE__) . '/schedules.inc';
+
+  // Set the message mode to drush output.
+  _backup_migrate_message_callback('_backup_migrate_message_drush');
+
+  if (!($schedule = backup_migrate_get_schedule($schedule_id))) {
+    _backup_migrate_message("Could not find the schedule '@schedule'. Try using 'drush bam-schedules' to get a list of available schedules.", array('@schedule' => $schedule_id), 'error');
+    return;
   }
 
-  backup_migrate_include('profiles', 'destinations');
+  if (!$schedule->enabled) {
+    _backup_migrate_message("Nothing to do, the schedule '@schedule' is disabled.", array('@schedule' => $schedule_id), 'warning');
+    return;
+  }
+
+  _backup_migrate_message("Starting schedule '$schedule_id'...");
+
+  backup_migrate_schedule_run($schedule_id);
+}
+
+/**
+ * Get a list of available destinations.
+ */
+function backup_migrate_drush_schedules() {
+  require_once dirname(__FILE__) . '/schedules.inc';
+
+  $rows = array(array(dt('ID'), dt('Name')));
+  foreach (backup_migrate_get_schedules() as $schedule) {
+    $rows[] = array(
+      $schedule->get_id(),
+      $schedule->get_name(),
+    );
+  }
+  drush_print_table($rows, TRUE, array(32, 32));
+}
+
+/**
+ * Restore to the default database.
+ */
+function backup_migrate_drush_restore($source_id = '', $destination_id = '', $file_id = '') {
+  require_once dirname(__FILE__) . '/destinations.inc';
+  require_once dirname(__FILE__) . '/profiles.inc';
+  require_once dirname(__FILE__) . '/sources.inc';
 
   // Set the message mode to drush output.
   _backup_migrate_message_callback('_backup_migrate_message_drush');
 
-  if (!backup_migrate_get_destination($source_id)) {
+  if (!backup_migrate_get_source($source_id)) {
     _backup_migrate_message("Could not find the source '@source'. Try using 'drush bam-sources' to get a list of available sources or use 'db' to backup the Drupal database.", array('@source' => $source_id), 'error');
     return;
   }
@@ -129,14 +181,18 @@ function backup_migrate_drush_restore($source_id = '', $destination_id = '', $fi
     _backup_migrate_message("Could not find the destination '@destination'. Try using 'drush bam-destinations' to get a list of available destinations.", array('@destination' => $destination_id), 'error');
     return;
   }
-  if (!$file_id || !$file = backup_migrate_destination_get_file($destination_id, $file_id)) {
+  elseif (!$file_id || !$file = backup_migrate_destination_get_file($destination_id, $file_id)) {
     _backup_migrate_message("Could not find the file '@file'. Try using 'drush bam-backups @destination' to get a list of available backup files in this destination destinations.", array('@destination' => $destination_id, '@file' => $file_id), 'error');
     return;
   }
 
+  drush_print(dt('Restoring will delete some or all of your data and cannot be undone. ALWAYS TEST YOUR BACKUPS ON A NON-PRODUCTION SERVER!'));
+  if (!drush_confirm(dt('Are you sure you want to perform the restore?'))) {
+    return drush_user_abort();
+  }
   _backup_migrate_message('Starting restore...');
   $settings = array('source_id' => $source_id);
-  backup_migrate_perform_restore($destination_id, $file_id);
+  backup_migrate_perform_restore($destination_id, $file_id, $settings);
 }
 
 /**
@@ -150,21 +206,38 @@ function backup_migrate_drush_destinations() {
  * Get a list of available sources.
  */
 function backup_migrate_drush_sources() {
-  return _backup_migrate_drush_destinations('source');
+  return _backup_migrate_drush_sources('source');
 }
 
-
 /**
  * Get a list of available destinations with the given op.
  */
 function _backup_migrate_drush_destinations($op = NULL) {
-  backup_migrate_include('destinations');
+  require_once dirname(__FILE__) . '/destinations.inc';
+
   $rows = array(array(dt('ID'), dt('Name'), dt('Operations')));
   foreach (backup_migrate_get_destinations($op) as $destination) {
     $rows[] = array(
       $destination->get_id(),
       $destination->get_name(),
-      implode (', ', $destination->ops()),
+      implode(', ', $destination->ops()),
+    );
+  }
+  drush_print_table($rows, TRUE, array(32, 32));
+}
+
+/**
+ * Get a list of available destinations with the given op.
+ */
+function _backup_migrate_drush_sources($op = NULL) {
+  require_once dirname(__FILE__) . '/sources.inc';
+
+  $rows = array(array(dt('ID'), dt('Name'), dt('Operations')));
+  foreach (backup_migrate_get_sources($op) as $destination) {
+    $rows[] = array(
+      $destination->get_id(),
+      $destination->get_name(),
+      implode(', ', $destination->ops()),
     );
   }
   drush_print_table($rows, TRUE, array(32, 32));
@@ -174,7 +247,8 @@ function _backup_migrate_drush_destinations($op = NULL) {
  * Get a list of available profiles.
  */
 function backup_migrate_drush_profiles() {
-  backup_migrate_include('profiles');
+  require_once dirname(__FILE__) . '/profiles.inc';
+
   $rows = array(array(dt('ID'), dt('Name')));
   foreach (backup_migrate_get_profiles() as $profile) {
     $rows[] = array(
@@ -186,46 +260,60 @@ function backup_migrate_drush_profiles() {
 }
 
 /**
- * Get a list of files in a given destination
+ * Get a list of files in a given destination.
  */
 function backup_migrate_drush_destination_files($destination_id = NULL) {
-  backup_migrate_include('destinations');
+  require_once dirname(__FILE__) . '/destinations.inc';
+
+  $destinations = array();
 
   // Set the message mode to drush output.
   _backup_migrate_message_callback('_backup_migrate_message_drush');
-
-  if (!$destination_id) {
-    _backup_migrate_message("You must specify an existing destination. Try using 'drush bam-destinations' to get a list of available destinations.", array('@destination' => $destination_id), 'error');
-    return;
-  }
-  if (!$destination = backup_migrate_get_destination($destination_id)) {
+  if ($destination_id && !$destination = backup_migrate_get_destination($destination_id)) {
     _backup_migrate_message("Could not find the destination '@destination'. Try using 'drush bam-destinations' to get a list of available destinations.", array('@destination' => $destination_id), 'error');
     return;
   }
 
-  $out = array(array(
-    dt('Filename'),
-    dt('Date'),
-    dt('Age'),
-    dt('Size'),
-  ));
+  // Single destination required.
+  if ($destination) {
+    $destinations = array($destination);
+  }
+  // List all destinations.
+  else {
+    $destinations = backup_migrate_get_destinations('list files');
+  }
 
-  $files      = $destination->list_files();
-  $i          = 0;
-  foreach ((array)$files as $file) {
-    // Show only files that can be restored from.
-    if ($file->is_recognized_type()) {
+  // Load all the files.
+  $rows = $sort = array();
+  foreach ($destinations as $destination) {
+    $destination->file_cache_clear();
+    $dest_files = $destination->list_files();
+    foreach ($dest_files as $id => $file) {
       $info = $file->info();
-      $out[] = array(
+      $rows[] = array(
         check_plain($info['filename']),
+        $destination->get_id(),
         format_date($info['filetime'], 'small'),
         format_interval(time() - $info['filetime'], 1),
         format_size($info['filesize']),
       );
+      $sort[] = $info['filetime'];
     }
   }
-  if (count($out) > 1) {
-    drush_print_table($out, TRUE);
+
+  $headers = array(
+    array(
+      dt('Filename'),
+      dt('Destination'),
+      dt('Date'),
+      dt('Age'),
+      dt('Size'),
+    ),
+  );
+
+  if (count($rows)) {
+    array_multisort($sort, SORT_DESC, $rows);
+    drush_print_table(array_merge($headers, $rows), TRUE);
   }
   else {
     drush_print(dt('There are no backup files to display.'));
@@ -236,8 +324,15 @@ function backup_migrate_drush_destination_files($destination_id = NULL) {
  * Send a message to the drush log.
  */
 function _backup_migrate_message_drush($message, $replace, $type) {
-  // Use drush_log to display to the user.
-  drush_log(strip_tags(dt($message, $replace)), str_replace('status', 'notice', $type));
+  // If this is an error use drush_set_error to notify the end user and set the
+  // exit status.
+  if ($type == 'error') {
+    drush_set_error(strip_tags(dt($message, $replace)));
+  }
+  else {
+    // Use drush_log to display to the user.
+    drush_log(strip_tags(dt($message, $replace)), str_replace('status', 'notice', $type));
+  }
   // Watchdog log the message as well for admins.
   _backup_migrate_message_log($message, $replace, $type);
 }

File diff suppressed because it is too large
+ 493 - 156
sites/all/modules/contrib/admin/backup_migrate/includes/crud.inc


+ 38 - 14
sites/all/modules/contrib/admin/backup_migrate/includes/destinations.browser.inc

@@ -1,6 +1,5 @@
 <?php
 
-
 /**
  * @file
  * Functions to handle the browser upload/download backup destination.
@@ -12,13 +11,15 @@
  * @ingroup backup_migrate_destinations
  */
 class backup_migrate_destination_browser extends backup_migrate_destination {
+
   /**
    * Get a row of data to be used in a list of items of this type.
-   */  
-  function get_list_row() {
+   */
+  public function get_list_row() {
     // Return none as this type should not be displayed.
     return array();
   }
+
 }
 
 /**
@@ -27,19 +28,26 @@ class backup_migrate_destination_browser extends backup_migrate_destination {
  * @ingroup backup_migrate_destinations
  */
 class backup_migrate_destination_browser_upload extends backup_migrate_destination_browser {
-  var $supported_ops = array('restore');
-  function __construct() {
+
+  /**
+   * {@inheritdoc}
+   */
+  public $supported_ops = array('restore');
+
+  /**
+   * Constructor.
+   */
+  public function __construct() {
     $params = array();
     $params['name'] = "Upload";
-    $params['destination_id'] = 'upload';
+    $params['machine_name'] = 'upload';
     parent::__construct($params);
   }
 
   /**
    * File load destination callback.
    */
-  function load_file($file_id) {
-    backup_migrate_include('files');
+  public function load_file($file_id) {
     if ($file = file_save_upload('backup_migrate_restore_upload')) {
       $out = new backup_file(array('filepath' => $file->uri));
       backup_migrate_temp_files_add($file->uri);
@@ -47,6 +55,7 @@ class backup_migrate_destination_browser_upload extends backup_migrate_destinati
     }
     return NULL;
   }
+
 }
 
 /**
@@ -55,20 +64,35 @@ class backup_migrate_destination_browser_upload extends backup_migrate_destinati
  * @ingroup backup_migrate_destinations
  */
 class backup_migrate_destination_browser_download extends backup_migrate_destination_browser {
-  var $supported_ops = array('manual backup');
-  function __construct() {
+
+  /**
+   * {@inheritdoc}
+   */
+  public $supported_ops = array('manual backup');
+
+  /**
+   * Browser downloads must always be the last destination as they must end the
+   * current process when they are done.
+   */
+  public $weight = 1000;
+
+  /**
+   * Constructor.
+   */
+  public function __construct() {
     $params = array();
     $params['name'] = "Download";
-    $params['destination_id'] = 'download';
+    $params['machine_name'] = 'download';
     parent::__construct($params);
   }
 
   /**
    * File save destination callback.
    */
-  function save_file($file, $settings) {
-    backup_migrate_include('files');
+  public function save_file($file, $settings) {
+    require_once dirname(__FILE__) . '/files.inc';
+
     $file->transfer();
   }
-}
 
+}

+ 100 - 79
sites/all/modules/contrib/admin/backup_migrate/includes/destinations.db.inc

@@ -1,6 +1,5 @@
 <?php
 
-
 /**
  * @file
  * Functions to handle the direct to database destination.
@@ -12,22 +11,24 @@
  * @ingroup backup_migrate_destinations
  */
 class backup_migrate_destination_db extends backup_migrate_destination_remote {
-  var $supported_ops = array('scheduled backup', 'manual backup', 'configure', 'source');
-  var $db_target = 'default';
-  var $connection = null;
-
+  public $supported_ops = array('scheduled backup', 'manual backup', 'configure', 'source');
+  public $db_target = 'default';
+  public $connection = NULL;
 
-  function type_name() {
+  /**
+   *
+   */
+  public function type_name() {
     return t("Database");
   }
 
   /**
    * Save the info by importing it into the database.
    */
-  function save_file($file, $settings) {
-    backup_migrate_include('files');
+  public function save_file($file, $settings) {
+    require_once dirname(__FILE__) . '/files.inc';
 
-    // Set the source_id to the destination_id in the settings since for a restore, the source_id is the 
+    // Set the source_id to the destination_id in the settings since for a restore, the source_id is the
     // database that gets restored to.
     $settings->set_source($this->get_id());
 
@@ -40,10 +41,10 @@ class backup_migrate_destination_db extends backup_migrate_destination_remote {
   /**
    * Destination configuration callback.
    */
-  function edit_form() {
+  public function edit_form() {
     $form = parent::edit_form();
     $form['scheme']['#title'] = t('Database type');
-//    $form['scheme']['#options'] = array($GLOBALS['db_type'] => $GLOBALS['db_type']);
+    // $form['scheme']['#options'] = array($GLOBALS['db_type'] => $GLOBALS['db_type']);
     $form['scheme']['#description'] = t('The type of the database. Drupal only supports one database type at a time, so this must be the same as the current database type.');
     $form['path']['#title'] = t('Database name');
     $form['path']['#description'] = t('The name of the database. The database must exist, it will not be created for you.');
@@ -54,7 +55,7 @@ class backup_migrate_destination_db extends backup_migrate_destination_remote {
   /**
    * Validate the configuration form. Make sure the db info is valid.
    */
-  function edit_form_validate($form, &$form_state) {
+  public function edit_form_validate($form, &$form_state) {
     if (!preg_match('/[a-zA-Z0-9_\$]+/', $form_state['values']['path'])) {
       form_set_error('path', t('The database name is not valid.'));
     }
@@ -62,66 +63,74 @@ class backup_migrate_destination_db extends backup_migrate_destination_remote {
   }
 
   /**
-   * Get the form for the settings for this destination.
+   * Get the default settings for this object.
    *
-   * Return the default tables whose data can be ignored. These tables mostly contain
-   *  info which can be easily reproducted (such as cache or search index)
-   *  but also tables which can become quite bloated but are not necessarily extremely
-   *  important to back up or migrate during development (such ass access log and watchdog)
+   * @return array
+   *   The default tables whose data can be ignored. These tables mostly
+   *   contain info which can be easily reproducted (such as cache or search
+   *   index) but also tables which can become quite bloated but are not
+   *   necessarily extremely important to back up or migrate during development
+   *   (such as access log and watchdog).
    */
-  function backup_settings_default() {
-    $core = array(
-        'cache',
-        'cache_admin_menu',
-        'cache_browscap',
-        'cache_content',
-        'cache_filter',
-        'cache_calendar_ical',
-        'cache_location',
-        'cache_menu',
-        'cache_page',
-        'cache_reptag',
-        'cache_views',
-        'cache_views_data',
-        'cache_block',
-        'cache_update',
-        'cache_form',
-        'cache_bootstrap',
-        'cache_field',
-        'cache_image',
-        'cache_path',
-        'sessions',
-        'search_dataset',
-        'search_index',
-        'search_keywords_log',
-        'search_total',
-        'watchdog',
-        'accesslog',
-        'devel_queries',
-        'devel_times',
-      );
-    $nodata_tables = array_merge($core, module_invoke_all('devel_caches'));
-     return array(
-      'nodata_tables' => $nodata_tables,
-      'exclude_tables' => array(),
+  public function backup_settings_default() {
+    $all_tables = $this->_get_table_names();
+
+    // Basic modules that should be excluded.
+    $basic = array(
+      // Default core tables.
+      'accesslog',
+      'sessions',
+      'watchdog',
+      // Search module.
+      'search_dataset',
+      'search_index',
+      'search_keywords_log',
+      'search_total',
+      // Devel module.
+      'devel_queries',
+      'devel_times',
+    );
+
+    // Identify all cache tables.
+    $cache = array('cache');
+    foreach ($all_tables as $table_name) {
+      if (strpos($table_name, 'cache_') === 0) {
+        $cache[] = $table_name;
+      }
+    }
+
+    // Simpletest can create a lot of tables that do not need to be backed up,
+    // but all of them start with the string 'simpletest' so they can be easily
+    // excluded.
+    $simpletest = array();
+    foreach ($all_tables as $table_name) {
+      if (strpos($table_name, 'simpletest') === 0) {
+        $simpletest[] = $table_name;
+      }
+    }
+
+    return array(
+      'nodata_tables' => array_merge($basic, $cache, module_invoke_all('devel_caches')),
+      'exclude_tables' => $simpletest,
       'utils_lock_tables' => FALSE,
-   );
+    );
   }
 
   /**
    * Get the form for the backup settings for this destination.
    */
-  function backup_settings_form($settings) {
-    $tables  = $this->get_table_names();
+  public function backup_settings_form($settings) {
+    $objects = $this->get_object_names();
     $form['#description'] = t("You may omit specific tables, or specific table data from the backup file. Only omit data that you know you will not need such as cache data, or tables from other applications. Excluding tables can break your Drupal install, so <strong>do not change these settings unless you know what you're doing</strong>.");
     $form['exclude_tables'] = array(
       "#type" => "select",
       "#multiple" => TRUE,
       "#title" => t("Exclude the following tables altogether"),
-      "#options" => $tables,
+      "#options" => $objects,
       "#default_value" => $settings['exclude_tables'],
       "#description" => t("The selected tables will not be added to the backup file."),
     );
+    $tables = $this->get_table_names();
     $form['nodata_tables'] = array(
       "#type" => "select",
       "#multiple" => TRUE,
@@ -142,16 +151,15 @@ class backup_migrate_destination_db extends backup_migrate_destination_remote {
   /**
    * Backup from this source.
    */
-  function backup_to_file($file, $settings) {
+  public function backup_to_file($file, $settings) {
     $file->push_type($this->get_file_type_id());
 
     backup_migrate_filters_invoke_all('pre_backup', $this, $file, $settings);
-    //$this->lock_tables($settings);
-
+    // $this->lock_tables($settings);
     // Switch to a different db if specified.
     $success = $this->_backup_db_to_file($file, $settings);
 
-    //$this->unlock_tables($settings);
+    // $this->unlock_tables($settings);
     backup_migrate_filters_invoke_all('post_backup', $this, $file, $settings, $success);
 
     return $success ? $file : FALSE;
@@ -160,7 +168,7 @@ class backup_migrate_destination_db extends backup_migrate_destination_remote {
   /**
    * Restore to this source.
    */
-  function restore_from_file($file, &$settings) {
+  public function restore_from_file($file, &$settings) {
     $num = 0;
     $type = $this->get_file_type_id();
     // Open the file using the file wrapper. Check that the dump is of the right type (allow .sql for legacy reasons).
@@ -182,7 +190,7 @@ class backup_migrate_destination_db extends backup_migrate_destination_remote {
   /**
    * Get the db connection for the specified db.
    */
-  function _get_db_connection() {
+  public function _get_db_connection() {
     if (!$this->connection) {
       $target = $key = '';
       $parts = explode(':', $this->get_id());
@@ -195,13 +203,13 @@ class backup_migrate_destination_db extends backup_migrate_destination_remote {
       else {
         // If the url is specified build it into a connection info array.
         if (!empty($this->dest_url)) {
-          $info = array(
-            'driver'    => empty($this->dest_url['scheme'])   ? NULL : $this->dest_url['scheme'],
-            'host'      => empty($this->dest_url['host'])     ? NULL : $this->dest_url['host'],
-            'port'      => empty($this->dest_url['port'])     ? NULL : $this->dest_url['port'],
-            'username'  => empty($this->dest_url['user'])     ? NULL : $this->dest_url['user'],
-            'password'  => empty($this->dest_url['pass'])     ? NULL : $this->dest_url['pass'],
-            'database'  => empty($this->dest_url['path'])     ? NULL : $this->dest_url['path'], 
+          $info   = array(
+            'driver'    => empty($this->dest_url['scheme']) ? NULL : $this->dest_url['scheme'],
+            'host'      => empty($this->dest_url['host']) ? NULL : $this->dest_url['host'],
+            'port'      => empty($this->dest_url['port']) ? NULL : $this->dest_url['port'],
+            'username'  => empty($this->dest_url['user']) ? NULL : $this->dest_url['user'],
+            'password'  => empty($this->dest_url['pass']) ? NULL : $this->dest_url['pass'],
+            'database'  => empty($this->dest_url['path']) ? NULL : $this->dest_url['path'],
           );
           $key    = uniqid('backup_migrate_tmp_');
           $target = 'default';
@@ -222,21 +230,33 @@ class backup_migrate_destination_db extends backup_migrate_destination_remote {
   /**
    * Backup the databases to a file.
    */
-  function _backup_db_to_file($file, $settings) {
+  public function _backup_db_to_file($file, $settings) {
     // Must be overridden.
   }
 
   /**
    * Backup the databases to a file.
    */
-  function _restore_db_from_file($file, $settings) {
+  public function _restore_db_from_file($file, $settings) {
     // Must be overridden.
   }
 
+  /**
+   * Get a list of objects in the database.
+   */
+  public function get_object_names() {
+    // Must be overridden.
+    $out = $this->_get_table_names();
+    if (method_exists($this, '_get_view_names')) {
+      $out += $this->_get_view_names();
+    }
+    return $out;
+  }
+
   /**
    * Get a list of tables in the database.
    */
-  function get_table_names() {
+  public function get_table_names() {
     // Must be overridden.
     $out = $this->_get_table_names();
     return $out;
@@ -245,7 +265,7 @@ class backup_migrate_destination_db extends backup_migrate_destination_remote {
   /**
    * Get a list of tables in the database.
    */
-  function _get_table_names() {
+  public function _get_table_names() {
     // Must be overridden.
     return array();
   }
@@ -253,12 +273,12 @@ class backup_migrate_destination_db extends backup_migrate_destination_remote {
   /**
    * Lock the database in anticipation of a backup.
    */
-  function lock_tables($settings) {
+  public function lock_tables($settings) {
     if ($settings->filters['utils_lock_tables']) {
       $tables = array();
       foreach ($this->get_table_names() as $table) {
         // There's no need to lock excluded or structure only tables because it doesn't matter if they change.
-        if (empty($settings->filters['exclude_tables']) || !in_array($table, (array)$settings->filters['exclude_tables'])) {
+        if (empty($settings->filters['exclude_tables']) || !in_array($table, (array) $settings->filters['exclude_tables'])) {
           $tables[] = $table;
         }
       }
@@ -269,14 +289,14 @@ class backup_migrate_destination_db extends backup_migrate_destination_remote {
   /**
    * Lock the list of given tables in the database.
    */
-  function _lock_tables($tables) {
+  public function _lock_tables($tables) {
     // Must be overridden.
   }
 
   /**
    * Unlock any tables that have been locked.
    */
-  function unlock_tables($settings) {
+  public function unlock_tables($settings) {
     if ($settings->filters['utils_lock_tables']) {
       $this->_unlock_tables();
     }
@@ -285,14 +305,15 @@ class backup_migrate_destination_db extends backup_migrate_destination_remote {
   /**
    * Unlock the list of given tables in the database.
    */
-  function _unlock_tables($tables) {
+  public function _unlock_tables($tables) {
     // Must be overridden.
   }
 
   /**
    * Get the file type for to backup this destination to.
    */
-  function get_file_type_id() {
+  public function get_file_type_id() {
     return 'sql';
   }
+
 }

+ 266 - 105
sites/all/modules/contrib/admin/backup_migrate/includes/destinations.db.mysql.inc

@@ -1,6 +1,10 @@
 <?php
 
-backup_migrate_include('destinations.db');
+/**
+ * @file
+ */
+
+require_once dirname(__FILE__) . '/destinations.db.inc';
 
 /**
  * @file
@@ -12,16 +16,40 @@ backup_migrate_include('destinations.db');
  *
  * @ingroup backup_migrate_destinations
  */
-
 class backup_migrate_destination_db_mysql extends backup_migrate_destination_db {
-  function type_name() {
+
+  /**
+   * The table's data keyed by table name.
+   *
+   * @var array
+   */
+  protected static $tableData = array();
+
+  /**
+   * The tables keyed by name.
+   *
+   * @var array
+   */
+  protected static $tableNames = array();
+
+  /**
+   * The views keyed by name.
+   *
+   * @var array
+   */
+  protected static $viewNames = array();
+
+  /**
+   *
+   */
+  public function type_name() {
     return t("MySQL Database");
   }
 
   /**
    * Return a list of backup filetypes.
    */
-  function file_types() {
+  public function file_types() {
     return array(
       "sql" => array(
         "extension" => "sql",
@@ -41,17 +69,27 @@ class backup_migrate_destination_db_mysql extends backup_migrate_destination_db
   /**
    * Declare any mysql databases defined in the settings.php file as a possible destination.
    */
-  function destinations() {
+  public function destinations() {
     $out = array();
     global $databases;
-    foreach ((array)$databases as $db_key => $target) {
-      foreach ((array)$target as $tgt_key => $info) {
+    foreach ((array) $databases as $db_key => $target) {
+      foreach ((array) $target as $tgt_key => $info) {
         // Only mysql/mysqli supported by this destination.
         $key = $db_key . ':' . $tgt_key;
         if ($info['driver'] === 'mysql') {
-          $url = $info['driver'] . '://' . $info['username'] . ':' . $info['password'] . '@' . $info['host'] . (isset($info['port']) ? ':' . $info['port'] : '') . '/' . $info['database'];
+          // Compile the database connection string.
+          $url = 'mysql://';
+          $url .= urlencode($info['username']) . ':' . urlencode($info['password']);
+          $url .= '@';
+          $url .= urlencode($info['host']);
+          if (!empty($info['port'])) {
+            $url .= ':' . $info['port'];
+          }
+          $url .= '/' . urlencode($info['database']);
+
           if ($destination = backup_migrate_create_destination('mysql', array('url' => $url))) {
-            // Treat the default database differently because it is probably the only one available.
+            // Treat the default database differently because it is probably
+            // the only one available.
             if ($key == 'default:default') {
               $destination->set_id('db');
               $destination->set_name(t('Default Database'));
@@ -60,8 +98,8 @@ class backup_migrate_destination_db_mysql extends backup_migrate_destination_db
               $destination->remove_op('manual backup');
             }
             else {
-              $destination->set_id('db:'. $key);
-              $destination->set_name($key .": ". $destination->get_display_location());
+              $destination->set_id('db:' . $key);
+              $destination->set_name($key . ": " . $destination->get_display_location());
             }
             $out[$destination->get_id()] = $destination;
           }
@@ -74,35 +112,34 @@ class backup_migrate_destination_db_mysql extends backup_migrate_destination_db
   /**
    * Get the file type for to backup this destination to.
    */
-  function get_file_type_id() {
+  public function get_file_type_id() {
     return 'mysql';
   }
 
   /**
    * Get the form for the backup settings for this destination.
    */
-  function backup_settings_form($settings) {
+  public function backup_settings_form($settings) {
     $form = parent::backup_settings_form($settings);
 
     $form['use_mysqldump'] = array(
       "#type" => "checkbox",
       "#title" => t("Use mysqldump command"),
       "#default_value" => !empty($settings['use_mysqldump']),
-      "#description" => t("Use the mysqldump command line tool if available. This can be faster for large databases but will not work on all servers. EXPERIMENTAL"),
+      "#description" => t("Use the mysqldump command line tool if available. This can be faster for large databases but will not work on all servers. Also exporting SQL views is not really solid with this option. EXPERIMENTAL"),
     );
 
     return $form;
   }
 
-
   /**
    * Backup the databases to a file.
    *
    *  Returns a list of sql commands, one command per line.
    *  That makes it easier to import without loading the whole file into memory.
-   *  The files are a little harder to read, but human-readability is not a priority
+   *  The files are a little harder to read, but human-readability is not a priority.
    */
-  function _backup_db_to_file($file, $settings) {
+  public function _backup_db_to_file($file, $settings) {
     if (!empty($settings->filters['use_mysqldump']) && $this->_backup_db_to_file_mysqldump($file, $settings)) {
       return TRUE;
     }
@@ -113,6 +150,8 @@ class backup_migrate_destination_db_mysql extends backup_migrate_destination_db
     if ($file->open(TRUE)) {
       $file->write($this->_get_sql_file_header());
       $alltables = $this->_get_tables();
+      $allviews = $this->_get_views();
+
       foreach ($alltables as $table) {
         if (_backup_migrate_check_timeout()) {
           return FALSE;
@@ -125,6 +164,15 @@ class backup_migrate_destination_db_mysql extends backup_migrate_destination_db
           }
         }
       }
+      foreach ($allviews as $view) {
+        if (_backup_migrate_check_timeout()) {
+          return FALSE;
+        }
+        if ($view['name'] && !isset($exclude[$view['name']])) {
+          $file->write($this->_get_view_create_sql($view));
+        }
+
+      }
       $file->write($this->_get_sql_file_footer());
       $file->close();
       return $lines;
@@ -134,16 +182,14 @@ class backup_migrate_destination_db_mysql extends backup_migrate_destination_db
     }
   }
 
-
   /**
    * Backup the databases to a file using the mysqldump command.
    */
-  function _backup_db_to_file_mysqldump($file, $settings) {
+  public function _backup_db_to_file_mysqldump($file, $settings) {
     $success = FALSE;
     $nodata_tables = array();
     $alltables = $this->_get_tables();
 
-
     $command = 'mysqldump --result-file=%file --opt -Q --host=%host --port=%port --user=%user --password=%pass %db';
     $args = array(
       '%file' => $file->filepath(),
@@ -155,17 +201,19 @@ class backup_migrate_destination_db_mysql extends backup_migrate_destination_db
     );
 
     // Ignore the excluded and no-data tables.
+    $db = $this->dest_url['path'];
     if (!empty($settings->filters['exclude_tables'])) {
-      $db = $this->dest_url['path'];
-      foreach ((array)$settings->filters['exclude_tables'] as $table) {
+      foreach ((array) $settings->filters['exclude_tables'] as $table) {
         if (isset($alltables[$table])) {
-          $command .= ' --ignore-table='. $db .'.'. $table;
+          $command .= ' --ignore-table=' . $db . '.' . $table;
         }
       }
-      foreach ((array)$settings->filters['nodata_tables'] as $table) {
+    }
+    if (!empty($settings->filters['nodata_tables'])) {
+      foreach ((array) $settings->filters['nodata_tables'] as $table) {
         if (isset($alltables[$table])) {
           $nodata_tables[] = $table;
-          $command .= ' --ignore-table='. $db .'.'. $table;
+          $command .= ' --ignore-table=' . $db . '.' . $table;
         }
       }
     }
@@ -183,10 +231,19 @@ class backup_migrate_destination_db_mysql extends backup_migrate_destination_db
   /**
    * Backup the databases to a file.
    */
-  function _restore_db_from_file($file, $settings) {
+  public function _restore_db_from_file($file, $settings) {
     $num = 0;
 
     if ($file->open() && $conn = $this->_get_db_connection()) {
+      // Optionally drop all existing tables.
+      if (!empty($settings->filters['utils_drop_all_tables'])) {
+        $all_tables = $this->_get_tables();
+        $table_names = array_map('backup_migrate_array_name_value', $all_tables);
+        $table_list = join(', ', $table_names);
+        $stmt = $conn->prepare("DROP TABLE IF EXISTS $table_list;\n");
+        $stmt->execute();
+      }
+
       // Read one line at a time and run the query.
       while ($line = $this->_read_sql_command_from_file($file)) {
         if (_backup_migrate_check_timeout()) {
@@ -209,17 +266,16 @@ class backup_migrate_destination_db_mysql extends backup_migrate_destination_db
     return $num;
   }
 
-
   /**
    * Read a multiline sql command from a file.
    *
    * Supports the formatting created by mysqldump, but won't handle multiline comments.
    */
-  function _read_sql_command_from_file($file) {
+  public function _read_sql_command_from_file($file) {
     $out = '';
     while ($line = $file->read()) {
       $first2 = substr($line, 0, 2);
-      $first3 = substr($line, 0, 2);
+      $first3 = substr($line, 0, 3);
 
       // Ignore single line comments. This function doesn't support multiline comments or inline comments.
       if ($first2 != '--' && ($first2 != '/*' || $first3 == '/*!')) {
@@ -236,46 +292,90 @@ class backup_migrate_destination_db_mysql extends backup_migrate_destination_db
   /**
    * Get a list of tables in the database.
    */
-  function _get_table_names() {
-    $out = array();
-    foreach ($this->_get_tables() as $table) {
-      $out[$table['name']] = $table['name'];
+  public function _get_table_names() {
+    if (empty(static::$tableNames)) {
+      static::$tableNames = $this->query("SHOW FULL TABLES WHERE Table_Type = 'BASE TABLE'")
+        ->fetchAllKeyed(0, 0);
     }
-    return $out;
+
+    return static::$tableNames;
+  }
+
+  /**
+   * Get a list of views in the database.
+   */
+  public function _get_view_names() {
+    if (empty(static::$viewNames)) {
+      static::$viewNames = $this->query("SHOW FULL TABLES WHERE Table_Type = 'VIEW'")
+        ->fetchAllKeyed(0, 0);
+    }
+
+    return static::$viewNames;
   }
 
   /**
    * Lock the list of given tables in the database.
    */
-  function _lock_tables($tables) {
+  public function _lock_tables($tables) {
     if ($tables) {
       $tables_escaped = array();
       foreach ($tables as $table) {
-        $tables_escaped[] = '`'. db_escape_table($table) .'`  WRITE';
+        $tables_escaped[] = '`' . db_escape_table($table) . '`  WRITE';
       }
-      $this->query('LOCK TABLES '. implode(', ', $tables_escaped));
+      $this->query('LOCK TABLES ' . implode(', ', $tables_escaped));
     }
   }
 
   /**
    * Unlock all tables in the database.
    */
-  function _unlock_tables($settings) {
+  public function _unlock_tables($settings) {
     $this->query('UNLOCK TABLES');
   }
 
+  /**
+   * Get a list of table and view data in the db.
+   */
+  protected function get_table_data() {
+    if (empty(static::$tableData)) {
+      $tables = $this->query('SHOW TABLE STATUS')
+        ->fetchAll(PDO::FETCH_ASSOC);
+
+      foreach ($tables as $table) {
+        // Lowercase the keys because between Drupal 7.12 and 7.13/14 the
+        // default query behavior was changed.
+        // See: http://drupal.org/node/1171866
+        $table = array_change_key_case($table);
+        static::$tableData[$table['name']] = $table;
+      }
+    }
+
+    return static::$tableData;
+  }
+
   /**
    * Get a list of tables in the db.
    */
-  function _get_tables() {
+  public function _get_tables() {
     $out = array();
-    // get auto_increment values and names of all tables
-    $tables = $this->query("show table status", array(), array('fetch' => PDO::FETCH_ASSOC));
-    foreach ($tables as $table) {
-      // Lowercase the keys because between Drupal 7.12 and 7.13/14 the default query behavior was changed.
-      // See: http://drupal.org/node/1171866
-      $table = array_change_key_case($table);
-      $out[$table['name']] = $table;
+    foreach ($this->get_table_data() as $table) {
+      if (!empty($table['engine'])) {
+        $out[$table['name']] = $table;
+      }
+    }
+
+    return $out;
+  }
+
+  /**
+   * Get a list of views in the db.
+   */
+  public function _get_views() {
+    $out = array();
+    foreach ($this->get_table_data() as $table) {
+      if (empty($table['engine'])) {
+        $out[$table['name']] = $table;
+      }
     }
     return $out;
   }
@@ -283,86 +383,124 @@ class backup_migrate_destination_db_mysql extends backup_migrate_destination_db
   /**
    * Get the sql for the structure of the given table.
    */
-  function _get_table_structure_sql($table) {
+  public function _get_table_structure_sql($table) {
     $out = "";
-    $result = $this->query("SHOW CREATE TABLE `". $table['name'] ."`", array(), array('fetch' => PDO::FETCH_ASSOC));
+    $result = $this->query("SHOW CREATE TABLE `" . $table['name'] . "`", array(), array('fetch' => PDO::FETCH_ASSOC));
     foreach ($result as $create) {
       // Lowercase the keys because between Drupal 7.12 and 7.13/14 the default query behavior was changed.
       // See: http://drupal.org/node/1171866
       $create = array_change_key_case($create);
-      $out .= "DROP TABLE IF EXISTS `". $table['name'] ."`;\n";
+      $out .= "DROP TABLE IF EXISTS `" . $table['name'] . "`;\n";
       // Remove newlines and convert " to ` because PDO seems to convert those for some reason.
       $out .= strtr($create['create table'], array("\n" => ' ', '"' => '`'));
       if ($table['auto_increment']) {
-        $out .= " AUTO_INCREMENT=". $table['auto_increment'];
+        $out .= " AUTO_INCREMENT=" . $table['auto_increment'];
       }
       $out .= ";\n";
     }
     return $out;
   }
-  
+
   /**
-   *  Get the sql to insert the data for a given table
+   * Get the sql for the structure of the given table.
    */
-  function _dump_table_data_sql_to_file($file, $table) {
-    $rows_per_line = variable_get('backup_migrate_data_rows_per_line', 30);
-    $bytes_per_line = variable_get('backup_migrate_data_bytes_per_line', 2000);
-  
-    $lines = 0;
-    $data = $this->query("SELECT * FROM `". $table['name'] ."`", array(), array('fetch' => PDO::FETCH_ASSOC));
-    $rows = $bytes = 0;
+  public function _get_view_create_sql($view) {
+    $out = "";
+    // Switch SQL mode to get rid of "CREATE ALGORITHM..." what requires more permissions + troubles with the DEFINER user.
+    $sql_mode = $this->query("SELECT @@SESSION.sql_mode")->fetchField();
+    $this->query("SET sql_mode = 'ANSI'");
+    $result = $this->query("SHOW CREATE VIEW `" . $view['name'] . "`", array(), array('fetch' => PDO::FETCH_ASSOC));
+    $this->query("SET SQL_mode = :mode", array(':mode' => $sql_mode));
+    foreach ($result as $create) {
+      $out .= "DROP VIEW IF EXISTS `" . $view['name'] . "`;\n";
+      $out .= "SET sql_mode = 'ANSI';\n";
+      $out .= strtr($create['Create View'], "\n", " ") . ";\n";
+      $out .= "SET sql_mode = '$sql_mode';\n";
+    }
+    return $out;
+  }
+
+  /**
+   * Get the sql to insert the data for a given table.
+   */
+  public function _dump_table_data_sql_to_file($file, $table) {
+    $rows_per_query = variable_get('backup_migrate_data_rows_per_query', BACKUP_MIGRATE_DATA_ROWS_PER_QUERY);
+    $rows_per_line = variable_get('backup_migrate_data_rows_per_line', BACKUP_MIGRATE_DATA_ROWS_PER_LINE);
+    $bytes_per_line = variable_get('backup_migrate_data_bytes_per_line', BACKUP_MIGRATE_DATA_BYTES_PER_LINE);
 
-    // Escape backslashes, PHP code, special chars
+    if (variable_get('backup_migrate_verbose')) {
+      _backup_migrate_message('Table: %table', array('%table' => $table['name']), 'success');
+    }
+
+    // Escape backslashes, PHP code, special chars.
     $search = array('\\', "'", "\x00", "\x0a", "\x0d", "\x1a");
     $replace = array('\\\\', "''", '\0', '\n', '\r', '\Z');
-  
-    $line = array();
-    foreach ($data as $row) {
-      // DB Escape the values.
-      $items = array();
-      foreach ($row as $key => $value) {
-        $items[] = is_null($value) ? "null" : "'". str_replace($search, $replace, $value) ."'";
+
+    $lines = 0;
+    $from = 0;
+    $args = array('fetch' => PDO::FETCH_ASSOC);
+    while ($data = $this->query("SELECT * FROM `" . $table['name'] . "`", array(), $args, $from, $rows_per_query)) {
+      if ($data->rowCount() == 0) {
+        break;
       }
-  
-      // If there is a row to be added.
-      if ($items) {
-        // Start a new line if we need to.
-        if ($rows == 0) {
-          $file->write("INSERT INTO `". $table['name'] ."` VALUES ");
-          $bytes = $rows = 0;
-        }
-        // Otherwise add a comma to end the previous entry.
-        else {
-          $file->write(",");
+
+      $rows = $bytes = 0;
+
+      $line = array();
+      foreach ($data as $row) {
+        $from++;
+
+        // DB Escape the values.
+        $items = array();
+        foreach ($row as $key => $value) {
+          $items[] = is_null($value) ? "null" : "'" . str_replace($search, $replace, $value) . "'";
         }
-  
-        // Write the data itself.
-        $sql = implode(',', $items);
-        $file->write('('. $sql .')');
-        $bytes += strlen($sql);
-        $rows++;
-  
-        // Finish the last line if we've added enough items
-        if ($rows >= $rows_per_line || $bytes >= $bytes_per_line) {
-          $file->write(";\n");
-          $lines++;
-          $bytes = $rows = 0;
+
+        // If there is a row to be added.
+        if ($items) {
+          // Start a new line if we need to.
+          if ($rows == 0) {
+            $file->write("INSERT INTO `" . $table['name'] . "` VALUES ");
+            $bytes = $rows = 0;
+          }
+          // Otherwise add a comma to end the previous entry.
+          else {
+            $file->write(",");
+          }
+
+          // Write the data itself.
+          $sql = implode(',', $items);
+          $file->write('(' . $sql . ')');
+          $bytes += strlen($sql);
+          $rows++;
+
+          // Finish the last line if we've added enough items.
+          if ($rows >= $rows_per_line || $bytes >= $bytes_per_line) {
+            $file->write(";\n");
+            $lines++;
+            $bytes = $rows = 0;
+          }
         }
       }
+
+      // Finish any unfinished insert statements.
+      if ($rows > 0) {
+        $file->write(";\n");
+        $lines++;
+      }
     }
-    // Finish any unfinished insert statements.
-    if ($rows > 0) {
-      $file->write(";\n");
-      $lines++;
+
+    if (variable_get('backup_migrate_verbose')) {
+      _backup_migrate_message('Peak memory usage: %mem', array('%mem' => backup_migrate_get_peak_memory_usage() . 'MB'), 'success');
     }
-  
+
     return $lines;
   }
 
   /**
    * Get the db connection for the specified db.
    */
-  function _get_db_connection() {
+  public function _get_db_connection() {
     if (!$this->connection) {
       $this->connection = parent::_get_db_connection();
       // Set the sql mode because the default is ANSI,TRADITIONAL which is not aware of collation or storage engine.
@@ -372,11 +510,34 @@ class backup_migrate_destination_db_mysql extends backup_migrate_destination_db
   }
 
   /**
-   * Run a db query on this destination's db.
+   * Run a query on this destination's database using Drupal's MySQL engine.
+   *
+   * @param string $query
+   *   The query string.
+   * @param array $args
+   *   Arguments for the query.
+   * @param array $options
+   *   Options to pass to the query.
+   * @param int|null $from
+   *   The starting point for the query; when passed will perform a queryRange()
+   *   method instead of a regular query().
+   * @param int|null $count
+   *   The number of records to obtain from this query. Will be ignored if the
+   *   $from argument is empty.
+   *
+   * @see DatabaseConnection_mysql::query()
+   * @see DatabaseConnection_mysql::queryRange()
    */
-  function query($query, $args = array(), $options = array()) {
+  public function query($query, array $args = array(), array $options = array(), $from = NULL, $count = NULL) {
     if ($conn = $this->_get_db_connection()) {
-      return $conn->query($query, $args, $options);
+      // If no $from is passed in, just do a basic query.
+      if (is_null($from)) {
+        return $conn->query($query, $args, $options);
+      }
+      // The $from variable was passed in, so do a ranged query.
+      else {
+        return $conn->queryRange($query, $from, $count, $args, $options);
+      }
     }
   }
 
@@ -384,7 +545,7 @@ class backup_migrate_destination_db_mysql extends backup_migrate_destination_db
    * The header for the top of the sql dump file. These commands set the connection
    *  character encoding to help prevent encoding conversion issues.
    */
-  function _get_sql_file_header() {
+  public function _get_sql_file_header() {
     return "/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
 /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
 /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
@@ -392,17 +553,17 @@ class backup_migrate_destination_db_mysql extends backup_migrate_destination_db
 /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
 /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE=NO_AUTO_VALUE_ON_ZERO */;
 
-SET NAMES utf8;
+SET NAMES utf8mb4;
 
 ";
   }
-  
+
   /**
    * The footer of the sql dump file.
    */
-  function _get_sql_file_footer() {
+  public function _get_sql_file_footer() {
     return "
-  
+
 /*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
 /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
 /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
@@ -411,5 +572,5 @@ SET NAMES utf8;
 /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
 ";
   }
-}
 
+}

+ 80 - 43
sites/all/modules/contrib/admin/backup_migrate/includes/destinations.email.inc

@@ -1,6 +1,5 @@
 <?php
 
-
 /**
  * @file
  * Functions to handle the email backup destination.
@@ -12,14 +11,14 @@
  * @ingroup backup_migrate_destinations
  */
 class backup_migrate_destination_email extends backup_migrate_destination {
-  var $supported_ops = array('scheduled backup', 'manual backup', 'configure');
+  public $supported_ops = array('scheduled backup', 'manual backup', 'remote backup', 'configure');
 
   /**
    * Save to (ie. email the file) to the email destination.
    */
-  function save_file($file, $settings) {
+  public function save_file($file, $settings) {
     $size = filesize($file->filepath());
-    $max = variable_get('backup_migrate_max_email_size', 20971520);
+    $max = variable_get('backup_migrate_max_email_size', BACKUP_MIGRATE_MAX_EMAIL_SIZE);
     if ($size > $max) {
       _backup_migrate_message('Could not email the file @file because it is @size and Backup and Migrate only supports emailing files smaller than @max.', array('@file' => $file->filename(), '@size' => format_size($size), '@max' => format_size($max)), 'error');
       return FALSE;
@@ -34,14 +33,14 @@ class backup_migrate_destination_email extends backup_migrate_destination {
   /**
    * Get the form for the settings for this filter.
    */
-  function edit_form() {
+  public function edit_form() {
     $form = parent::edit_form();
     $form['location'] = array(
       "#type" => "textfield",
       "#title" => t("Email Address"),
       "#default_value" => $this->get_location(),
       "#required" => TRUE,
-      "#description" => t('Enter the email address to send the backup files to. Make sure the email sever can handle large file attachments'),
+      "#description" => t('Enter the email address to send the backup files to. Make sure the email server can handle large file attachments'),
     );
     return $form;
   }
@@ -49,11 +48,12 @@ class backup_migrate_destination_email extends backup_migrate_destination {
   /**
    * Validate the configuration form. Make sure the email address is valid.
    */
-  function settings_form_validate($values) {
+  public function settings_form_validate($values) {
     if (!valid_email_address($values['location'])) {
       form_set_error('[location]', t('The e-mail address %mail is not valid.', array('%mail' => $form_state['values']['location'])));
     }
   }
+
 }
 
 /**
@@ -72,69 +72,106 @@ class backup_migrate_destination_email extends backup_migrate_destination {
  *   filename and "filename" which is just the filename.
  */
 function _backup_migrate_destination_email_mail_backup($attachment, $to) {
-  // Send mail
+  // Send mail.
   $attach        = fread(fopen($attachment->path, "r"), filesize($attachment->path));
   $mail          = new mime_mail();
   $mail->from    = variable_get('site_mail', ini_get('sendmail_from'));
-  $mail->headers = 'Errors-To: [EMAIL='. $mail->from .']'. $mail->from .'[/EMAIL]';
+  $mail->headers = 'Errors-To: [EMAIL=' . $mail->from . ']' . $mail->from . '[/EMAIL]';
   $mail->to      = $to;
   $mail->subject = t('Database backup from !site: !file', array('!site' => variable_get('site_name', 'drupal'), '!file' => $attachment->filename));
-  $mail->body    = t('Database backup attached.') ."\n\n";
+  $mail->body    = t('Database backup attached.') . "\n\n";
 
-  $mail->add_attachment("$attach", $attachment->filename, "Content-Transfer-Encoding: base64 /9j/4AAQSkZJRgABAgEASABIAAD/7QT+UGhvdG9zaG", NULL, TRUE);
+  $mail->add_attachment($attach, $attachment->filename, "application/octet-stream");
   $mail->send();
 }
 
+/**
+ *
+ */
 class mime_mail {
-  var $parts;
-  var $to;
-  var $from;
-  var $headers;
-  var $subject;
-  var $body;
-
-  function mime_mail() {
+  public $parts;
+  public $to;
+  public $from;
+  public $headers;
+  public $subject;
+  public $body;
+
+  /**
+   *
+   */
+  public function __construct() {
     $this->parts   = array();
     $this->to      = "";
     $this->from    = "";
-    $this->headers = "";
+    $this->headers = array();
     $this->subject = "";
     $this->body    = "";
   }
 
-  function add_attachment($message, $name = "", $ctype = "application/octet-stream", $encode = NULL, $attach = FALSE) {
+  /**
+   *
+   */
+  public function add_attachment($message, $name, $ctype) {
     $this->parts[] = array(
-      "ctype" => $ctype,
       "message" => $message,
-      "encode" => $encode,
       "name" => $name,
-      "attach" => $attach,
+      "ctype" => $ctype,
     );
   }
 
-  function build_message($part) {
-    $message  = $part["message"];
-    $message  = chunk_split(base64_encode($message));
-    $encoding = "base64";
-    $disposition = $part['attach'] ? "Content-Disposition: attachment; filename=$part[name]\n" : '';
-    return "Content-Type: ". $part["ctype"] . ($part["name"] ? "; name = \"". $part["name"] ."\"" : "") ."\nContent-Transfer-Encoding: $encoding\n$disposition\n$message\n";
+  /**
+   *
+   */
+  public function build_message($part) {
+    $crlf = "\r\n";
+    // See RFC 2184.
+    $continuation = $crlf . '  ';
+    $name = $part['name'];
+    $len = strlen($name);
+
+    // RFC 5322 recommends lines of no longer than 78 chars, which in
+    // this case comes down to filenames of no longer than 64 chars.
+    if ($len > 64) {
+      // We want to preserve the time stamp and extension and such.
+      $head = substr($name, 0, 28);
+      $tail = substr($name, ($len - 32));
+      $name = $head . '___' . $tail;
+    }
+    $message = chunk_split(base64_encode($part["message"]), 70, $crlf);
+    $disposition = $name ? "Content-Disposition: attachment; {$continuation}filename=\"$name\"$crlf" : "";
+    return "Content-Type: " . $part["ctype"] . ($name ? ";{$continuation}name=\"$name\"" : "") .
+    "{$crlf}Content-Transfer-Encoding: base64$crlf$disposition$crlf$message";
   }
 
-  function build_multipart() {
-    $boundary = "b". md5(uniqid(time()));
-    $multipart = "Content-Type: multipart/mixed; boundary = $boundary\n\nThis is a MIME encoded message.\n\n--$boundary";
-    for ($i = sizeof($this->parts) - 1; $i >= 0; $i--) {
-      $multipart .= "\n". $this->build_message($this->parts[$i]) ."--$boundary";
+  /**
+   *
+   */
+  public function build_multipart($boundary) {
+    $multipart = "This is a MIME encoded message.\r\n\r\n--$boundary";
+    for ($i = count($this->parts) - 1; $i >= 0; $i--) {
+      $multipart .= "\r\n" . $this->build_message($this->parts[$i]) . "--$boundary";
     }
-    return $multipart .= "--\n";
+    return $multipart . "--\r\n";
   }
 
-  function send() {
-    $mime = "";
-    if (!empty($this->from)) $mime .= "From: ". $this->from ."\n";
-    if (!empty($this->headers)) $mime .= $this->headers ."\n";
-    if (!empty($this->body)) $this->add_attachment($this->body, "", "text/plain");
-    $mime .= "MIME-Version: 1.0\n". $this->build_multipart();
-    mail($this->to, $this->subject, "", $mime);
+  /**
+   *
+   */
+  public function send() {
+    $headers = array();
+    if (!empty($this->body)) {
+      $this->add_attachment($this->body, "", "text/plain");
+    }
+    $headers['MIME-Version'] = "1.0";
+    $boundary = "b" . md5(uniqid(time()));
+    $headers['Content-Type'] = "multipart/mixed; boundary=\"$boundary\"";
+    $message = $this->build_multipart($boundary);
+    $params = array();
+    $params['body'] = $message;
+    $params['headers'] = $headers;
+    $params['subject'] = $this->subject;
+
+    drupal_mail('backup_migrate', 'destination_mail', trim($this->to), '', $params, $this->from);
   }
+
 }

+ 145 - 78
sites/all/modules/contrib/admin/backup_migrate/includes/destinations.file.inc

@@ -1,6 +1,5 @@
 <?php
 
-
 /**
  * @file
  * A destination type for saving locally to the server.
@@ -11,79 +10,104 @@
  *
  * @ingroup backup_migrate_destinations
  */
-
 class backup_migrate_destination_files extends backup_migrate_destination {
-  var $supported_ops = array('scheduled backup', 'manual backup', 'restore', 'list files', 'configure', 'delete');
+  public $supported_ops = array('scheduled backup', 'manual backup', 'local backup', 'restore', 'list files', 'configure', 'delete');
 
-  function type_name() {
+  /**
+   *
+   */
+  public function type_name() {
     return t("Server Directory");
   }
 
   /**
    * Get the file location.
    */
-  function get_realpath() {
-    return drupal_realpath($this->get_location());
+  public function get_realpath() {
+    if ($realpath = drupal_realpath($this->get_location())) {
+      return $realpath;
+    }
+    return $this->get_location();
   }
 
   /**
    * File save destination callback.
    */
-  function _save_file($file, $settings) {
-    if ($dir = $this->get_location()) {
-      if ($dir = $this->check_dir($dir)) {
-        $filepath = rtrim($dir, "/") ."/". $file->filename();
-        if (file_unmanaged_move($file->filepath(), $filepath)) {
-  
-          // chmod, chown and chgrp the file if needed.
-          if ($chmod = $this->settings('chmod')) {
-            if (!@drupal_chmod($filepath, octdec($chmod))) {
-              _backup_migrate_message('Unable to set the file mode for: @file', array('@file' => $filepath), 'error');
-            }
-          }
-          if ($chgrp = $this->settings('chgrp')) {
-            if (!@chgrp($filepath, $chgrp)) {
-              _backup_migrate_message('Unable to set the file group for: @file', array('@file' => $filepath), 'error');
-            }
-          }
-          return $file;
+  public function _save_file($file, $settings) {
+    if ($this->confirm_destination() && $dir = $this->get_location()) {
+      $filepath = rtrim($dir, "/") . "/" . $file->filename();
+
+      // Allow files to be overwritten by the filesystem.
+      $replace_method = $settings->append_timestamp == 2 ? FILE_EXISTS_REPLACE : FILE_EXISTS_RENAME;
+
+      // Copy the file if there are multiple destinations.
+      if (count($settings->get_destinations()) > 1) {
+        file_unmanaged_copy($file->filepath(), $filepath, $replace_method);
+      }
+      // Otherwise we can move it and save a delete.
+      else {
+        file_unmanaged_move($file->filepath(), $filepath, $replace_method);
+      }
+
+      // chmod, chown and chgrp the file if needed.
+      if ($chmod = $this->settings('chmod')) {
+        if (!@drupal_chmod($filepath, octdec($chmod))) {
+          _backup_migrate_message('Unable to set the file mode for: @file', array('@file' => $filepath), 'error');
         }
-        else {
-          _backup_migrate_message('Unable to save the file to the directory: @dir', array('@dir' => $dir), 'error');
+      }
+      if ($chgrp = $this->settings('chgrp')) {
+        if (!@chgrp($filepath, $chgrp)) {
+          _backup_migrate_message('Unable to set the file group for: @file', array('@file' => $filepath), 'error');
         }
       }
+      return $file;
     }
   }
 
   /**
    * Determine if we can read the given file.
    */
-  function can_read_file($file_id) {
+  public function can_read_file($file_id) {
     return $this->op('restore') && is_readable($this->get_filepath($file_id));
   }
 
   /**
    * File load destination callback.
    */
-  function load_file($file_id) {
+  public function load_file($file_id) {
     $filepath = $this->get_filepath($file_id);
     if (file_exists($filepath)) {
-      backup_migrate_include('files');
+      require_once dirname(__FILE__) . '/files.inc';
+
       return new backup_file(array('filepath' => $filepath));
     }
   }
 
+  /**
+   * Get the file object for the given file.
+   */
+  public function get_file($file_id) {
+    $files = $this->list_files();
+    if (isset($files[$file_id])) {
+      isset($files[$file_id]);
+    }
+    return NULL;
+  }
+
   /**
    * File list destination callback.
    */
-  function _list_files() {
+  public function _list_files() {
     $files = array();
     if ($dir = $this->get_realpath()) {
       if ($handle = @opendir($dir)) {
-        backup_migrate_include('files');
+        require_once dirname(__FILE__) . '/files.inc';
+
         while (FALSE !== ($file = readdir($handle))) {
-          $filepath = $dir ."/". $file;
-          $files[$file] = new backup_file(array('filepath' => $filepath));
+          if (substr($file, 0, 1) !== '.') {
+            $filepath = $dir . "/" . $file;
+            $files[$file] = new backup_file(array('filepath' => $filepath));
+          }
         }
       }
     }
@@ -93,7 +117,7 @@ class backup_migrate_destination_files extends backup_migrate_destination {
   /**
    * File delete destination callback.
    */
-  function _delete_file($file_id) {
+  public function _delete_file($file_id) {
     $filepath = $this->get_filepath($file_id);
     file_unmanaged_delete($filepath);
   }
@@ -101,9 +125,9 @@ class backup_migrate_destination_files extends backup_migrate_destination {
   /**
    * Get the filepath from the given file id.
    */
-  function get_filepath($file_id) {
+  public function get_filepath($file_id) {
     if ($dir = $this->get_realpath()) {
-      $filepath = rtrim($dir, '/') .'/'. $file_id;
+      $filepath = rtrim($dir, '/') . '/' . $file_id;
       return $filepath;
     }
     return FALSE;
@@ -112,7 +136,7 @@ class backup_migrate_destination_files extends backup_migrate_destination {
   /**
    * Get the form for the settings for the files destination.
    */
-  function edit_form() {
+  public function edit_form() {
     $form = parent::edit_form();
     $form['location'] = array(
       "#type" => "textfield",
@@ -152,7 +176,7 @@ class backup_migrate_destination_files extends backup_migrate_destination {
   /**
    * Validate the form for the settings for the files destination.
    */
-  function edit_form_validate($form, &$form_state) {
+  public function edit_form_validate($form, &$form_state) {
     $values = $form_state['values'];
     if (isset($values['settings']['chmod']) && !empty($values['settings']['chmod']) && !preg_match('/0?[0-7]{3}/', $values['settings']['chmod'])) {
       form_set_error('chmod', t('You must enter a valid chmod octal value (e.g. 644 or 0644) in the change mode field, or leave it blank.'));
@@ -163,7 +187,7 @@ class backup_migrate_destination_files extends backup_migrate_destination {
   /**
    * Submit the form for the settings for the files destination.
    */
-  function edit_form_submit($form, &$form_state) {
+  public function edit_form_submit($form, &$form_state) {
     // Add a 0 to the start of a 3 digit file mode to make it proper PHP encoded octal.
     if (strlen($form_state['values']['settings']['chmod']) == 3) {
       $form_state['values']['settings']['chmod'] = '0' . $form_state['values']['settings']['chmod'];
@@ -171,15 +195,26 @@ class backup_migrate_destination_files extends backup_migrate_destination {
     parent::edit_form_submit($form, $form_state);
   }
 
+  /**
+   * Check that a destination is valid.
+   */
+  public function confirm_destination() {
+    if ($dir = $this->get_location()) {
+      return $this->check_dir($dir);
+    }
+    return FALSE;
+  }
+
   /**
    * Prepare the destination directory for the backups.
    */
-  function check_dir($directory) {
+  public function check_dir($directory) {
     if (!file_prepare_directory($directory, FILE_CREATE_DIRECTORY)) {
       // Unable to create destination directory.
-      _backup_migrate_message("Unable to create or write to the save directory '%directory'. Please check the file permissions that directory and try again.", array('%directory' => $directory), "error");
+      _backup_migrate_message("Unable to create or write to the save directory '%directory'. Please check the file permissions of that directory and try again.", array('%directory' => $directory), "error");
       return FALSE;
     }
+
     // If the destination directory is within the webroot, then secure it as best we can.
     if ($this->dir_in_webroot($directory)) {
       $directory = $this->check_web_dir($directory);
@@ -191,45 +226,60 @@ class backup_migrate_destination_files extends backup_migrate_destination {
   /**
    * Check that a web accessible directory has been properly secured, othewise attempt to secure it.
    */
-  function check_web_dir($directory) {
-    file_create_htaccess($directory, TRUE);
-
-    // Check the user agent to make sure we're not responding to a request from drupal itself.
-    // That should prevent infinite loops which could be caused by poormanscron in some circumstances.
-    if (strpos($_SERVER['HTTP_USER_AGENT'], 'Drupal') !== FALSE) {
-      return FALSE;
+  public function check_web_dir($directory) {
+    // Check if the file has already been tested.
+    if (is_file($directory . '/tested.txt')) {
+      return $directory;
     }
+    else {
+      file_create_htaccess($directory, TRUE);
 
-    // Check to see if the destination is publicly accessible
-    $test_contents = "this file should not be publicly accessible";
-    // Create the the text.txt file if it's not already there.
-    if (!is_file($directory .'/test.txt') || file_get_contents($directory .'/test.txt') != $test_contents) {
-      if ($fp = fopen($directory .'/test.txt', 'w')) {
-        @fputs($fp, $test_contents);
-        fclose($fp);
+      // Check the user agent to make sure we're not responding to a request from drupal itself.
+      // That should prevent infinite loops which could be caused by poormanscron in some circumstances.
+      if (strpos($_SERVER['HTTP_USER_AGENT'], 'Drupal') !== FALSE) {
+        return FALSE;
       }
-      else {
-        $message = t("Security notice: Backup and Migrate was unable to write a test text file to the destination directory %directory, and is therefore unable to check the security of the backup destination. Backups to the server will be disabled until the destination becomes writable and secure.", array('%directory' => $directory));
+
+      // Check to see if the destination is publicly accessible.
+      $test_contents = "this file should not be publicly accessible";
+      // Create the the text.txt file if it's not already there.
+      if (!is_file($directory . '/test.txt') || file_get_contents($directory . '/test.txt') != $test_contents) {
+        if ($fp = fopen($directory . '/test.txt', 'w')) {
+          @fputs($fp, $test_contents);
+          fclose($fp);
+        }
+        else {
+          $message = t("Security notice: Backup and Migrate was unable to write a test text file to the destination directory %directory, and is therefore unable to check the security of the backup destination. Backups to the server will be disabled until the destination becomes writable and secure.", array('%directory' => $directory));
+          drupal_set_message($message, "error");
+          return FALSE;
+        }
+      }
+
+      // Attempt to read the test file via http. This may fail for other
+      // reasons, so it's not a bullet-proof check.
+      if ($this->test_file_readable_remotely($directory . '/test.txt', $test_contents)) {
+        $message = t("Security notice: Backup and Migrate will not save backup files to the server because the destination directory is publicly accessible. If you want to save files to the server, please secure the '%directory' directory", array('%directory' => $directory));
         drupal_set_message($message, "error");
+        unlink($directory . '/test.txt');
         return FALSE;
       }
+      // Directory tested OK, so we mark it as tested.
+      if ($fp = fopen($directory . '/tested.txt', 'w')) {
+        $contents = t('The presence of this file indicates that this directory has been tested as safe to use as a destination for Backup and Migrate. If you change the permissions of this directory or change your web server settings, please delete this file so that the directory can be checked again.');
+        @fputs($fp, $contents);
+        fclose($fp);
+      }
+      return $directory;
     }
-
-    // Attempt to read the test file via http. This may fail for other reasons,
-    // so it's not a bullet-proof check.
-    if ($this->test_file_readable_remotely($directory .'/test.txt', $test_contents)) {
-      $message = t("Security notice: Backup and Migrate will not save backup files to the server because the destination directory is publicly accessible. If you want to save files to the server, please secure the '%directory' directory", array('%directory' => $directory));
-      drupal_set_message($message, "error");
-      return FALSE;
-    }
-    return $directory;
   }
 
   /**
    * Check if the given directory is within the webroot and is therefore web accessible.
    */
-  function dir_in_webroot($directory) {
-    if (strpos(drupal_realpath($directory), realpath($_SERVER['DOCUMENT_ROOT'])) !== FALSE) {
+  public function dir_in_webroot($directory) {
+    $real_dir = drupal_realpath($directory);
+    $real_root = drupal_realpath(DRUPAL_ROOT);
+    if ($real_dir == $real_root || strpos($real_dir, $real_root . '/') === 0) {
       return TRUE;
     }
     return FALSE;
@@ -238,35 +288,52 @@ class backup_migrate_destination_files extends backup_migrate_destination {
   /**
    * Check if a file can be read remotely via http.
    */
-  function test_file_readable_remotely($path, $contents) {
-    $url = file_create_url($path);
-    // TODO: Proper checking for absolute paths.
-    $result = drupal_http_request($url);
-    if (!empty($result->data) && strpos($result->data, $contents) !== FALSE) {
-      return TRUE;
+  public function test_file_readable_remotely($directory, $contents) {
+    $real_dir = drupal_realpath($directory);
+    $real_root = drupal_realpath(DRUPAL_ROOT);
+    if ($real_dir && $real_root) {
+      // Get the root relative path.
+      $path = substr($real_dir, strlen($real_root));
+
+      $url = $GLOBALS['base_url'] . str_replace('\\', '/', $path);
+      $result = drupal_http_request($url);
+      if (!empty($result->data) && strpos($result->data, $contents) !== FALSE) {
+        return TRUE;
+      }
     }
     return FALSE;
   }
+
 }
 
 /**
  * The manual files directory.
  */
 class backup_migrate_destination_files_manual extends backup_migrate_destination_files {
-  var $supported_ops = array('manual backup', 'restore', 'list files', 'configure', 'delete');
-  function __construct($params = array()) {
+  public $supported_ops = array('manual backup', 'restore', 'list files', 'configure', 'delete');
+
+  /**
+   *
+   */
+  public function __construct($params = array()) {
     $dir = 'private://backup_migrate/manual';
     parent::__construct($params + array('location' => $dir, 'name' => t('Manual Backups Directory')));
   }
+
 }
 
 /**
  * The scheduled files directory.
  */
 class backup_migrate_destination_files_scheduled extends backup_migrate_destination_files {
-  var $supported_ops = array('scheduled backup', 'restore', 'list files', 'configure', 'delete');
-  function __construct($params = array()) {
+  public $supported_ops = array('scheduled backup', 'restore', 'list files', 'configure', 'delete');
+
+  /**
+   *
+   */
+  public function __construct($params = array()) {
     $dir = 'private://backup_migrate/scheduled';
     parent::__construct($params + array('location' => $dir, 'name' => t('Scheduled Backups Directory')));
   }
+
 }

+ 75 - 68
sites/all/modules/contrib/admin/backup_migrate/includes/destinations.ftp.inc

@@ -1,6 +1,5 @@
 <?php
 
-
 /**
  * @file
  * Functions to handle the FTP backup destination.
@@ -12,13 +11,13 @@
  * @ingroup backup_migrate_destinations
  */
 class backup_migrate_destination_ftp extends backup_migrate_destination_remote {
-  var $supported_ops = array('scheduled backup', 'manual backup', 'restore', 'list files', 'configure', 'delete');
-  var $ftp = NULL;
+  public $supported_ops = array('scheduled backup', 'manual backup', 'remote backup', 'restore', 'list files', 'configure', 'delete');
+  public $ftp = NULL;
 
   /**
    * Save to the ftp destination.
    */
-  function _save_file($file, $settings) {
+  public function _save_file($file, $settings) {
     $ftp = $this->ftp_object();
     if (drupal_ftp_file_to_ftp($file->filepath(), $file->filename(), '.', $ftp)) {
       return $file;
@@ -29,8 +28,9 @@ class backup_migrate_destination_ftp extends backup_migrate_destination_remote {
   /**
    * Load from the ftp destination.
    */
-  function load_file($file_id) {
-    backup_migrate_include('files');
+  public function load_file($file_id) {
+    require_once dirname(__FILE__) . '/files.inc';
+
     $file = new backup_file(array('filename' => $file_id));
     $this->ftp_object();
     if (drupal_ftp_ftp_to_file($file->filepath(), $file_id, '.', $this->ftp)) {
@@ -42,16 +42,20 @@ class backup_migrate_destination_ftp extends backup_migrate_destination_remote {
   /**
    * Delete from the ftp destination.
    */
-  function _delete_file($file_id) {
+  public function _delete_file($file_id) {
     $this->ftp_object();
     drupal_ftp_delete_file($file_id, $this->ftp);
   }
 
-  function _list_files() {
-    backup_migrate_include('files');
+  /**
+   *
+   */
+  public function _list_files() {
+    require_once dirname(__FILE__) . '/files.inc';
+
     $files = array();
     $this->ftp_object();
-    $ftp_files = drupal_ftp_file_list('.', $this->ftp);
+    $ftp_files = (array) drupal_ftp_file_list('.', $this->ftp);
     foreach ($ftp_files as $file) {
       $files[$file['filename']] = new backup_file($file);
     }
@@ -61,7 +65,7 @@ class backup_migrate_destination_ftp extends backup_migrate_destination_remote {
   /**
    * Get the form for the settings for this filter.
    */
-  function edit_form() {
+  public function edit_form() {
     $form = parent::edit_form();
     $form['scheme']['#type'] = 'value';
     $form['scheme']['#value'] = 'ftp';
@@ -80,15 +84,24 @@ class backup_migrate_destination_ftp extends backup_migrate_destination_remote {
     return $form;
   }
 
-  function set_pasv($value) {
-    $this->settings['pasv'] = (bool)$value;
+  /**
+   *
+   */
+  public function set_pasv($value) {
+    $this->settings['pasv'] = (bool) $value;
   }
 
-  function get_pasv() {
+  /**
+   *
+   */
+  public function get_pasv() {
     return isset($this->settings['pasv']) ? $this->settings['pasv'] : FALSE;
   }
 
-  function ftp_object() {
+  /**
+   *
+   */
+  public function ftp_object() {
     if (!$this->ftp) {
       $this->dest_url['port'] = empty($this->dest_url['port']) ? '21' : $this->dest_url['port'];
       $this->dest_url['pasv'] = $this->get_pasv();
@@ -96,19 +109,18 @@ class backup_migrate_destination_ftp extends backup_migrate_destination_remote {
     }
     return $this->ftp;
   }
+
 }
 
 // The FTP code below was taken from the ftp module by Aaron Winborn.
-
 // Inspired by http://www.devarticles.com/c/a/PHP/My-FTP-Wrapper-Class-for-PHP/
 // It's been drupalized, however, and most of the bugs from that example have been fixed.
-// - winborn 2007-06-22 - 2007-06-28
-
+// - winborn 2007-06-22 - 2007-06-28.
 define('DRUPAL_FTP_FT_DIRECTORY', 0);
 define('DRUPAL_FTP_FT_FILE', 1);
 
 /**
- *  creates a new ftp object. if any elements of ftp_map are missing, they'll be filled with the server defaults.
+ * Creates a new ftp object. if any elements of ftp_map are missing, they'll be filled with the server defaults.
  */
 function drupal_ftp_ftp_object($server, $port, $user, $pass, $dir, $pasv) {
   $ftp = new stdClass();
@@ -124,17 +136,17 @@ function drupal_ftp_ftp_object($server, $port, $user, $pass, $dir, $pasv) {
 }
 
 /**
- *  The drupal_ftp_connect function
+ * The drupal_ftp_connect function
  *  This function connects to an FTP server and attempts to change into the directory specified by
  *  the fourth parameter, $directory.
  */
 function drupal_ftp_connect(&$ftp) {
-  if (is_NULL($ftp)) {
+  if (is_null($ftp)) {
     $ftp = drupal_ftp_ftp_object();
   }
 
-  if (empty($ftp->__conn) && !drupal_ftp_connected($ftp)) {
-    // Attempt to connect to the remote server
+  if (!$ftp->__conn && !drupal_ftp_connected($ftp)) {
+    // Attempt to connect to the remote server.
     $ftp->__conn = @ftp_connect($ftp->__server, $ftp->__port);
 
     if (!$ftp->__conn) {
@@ -142,7 +154,7 @@ function drupal_ftp_connect(&$ftp) {
       return FALSE;
     }
 
-    // Attempt to login to the remote server
+    // Attempt to login to the remote server.
     $ftp->__login = @ftp_login($ftp->__conn, $ftp->__user, $ftp->__password);
 
     if (!$ftp->__login) {
@@ -150,7 +162,7 @@ function drupal_ftp_connect(&$ftp) {
       return FALSE;
     }
 
-    // Attempt to change into the working directory
+    // Attempt to change into the working directory.
     $chdir = @ftp_chdir($ftp->__conn, $ftp->__directory);
 
     if (!$chdir) {
@@ -158,7 +170,7 @@ function drupal_ftp_connect(&$ftp) {
       return FALSE;
     }
 
-    // Set PASV - if needed
+    // Set PASV - if needed.
     if ($ftp->__pasv) {
       $pasv = @ftp_pasv($ftp->__conn, TRUE);
       if (!$pasv) {
@@ -168,7 +180,7 @@ function drupal_ftp_connect(&$ftp) {
     }
   }
 
-  // Everything worked OK, return TRUE
+  // Everything worked OK, return TRUE.
   return TRUE;
 }
 
@@ -179,38 +191,36 @@ function drupal_ftp_connect(&$ftp) {
  */
 function drupal_ftp_connected(&$ftp) {
   // Attempt to call the ftp_systype to see if the connect
-  // to the FTP server is still alive and kicking
-
-  if (is_NULL($ftp)) {
+  // to the FTP server is still alive and kicking.
+  if (is_null($ftp)) {
     $ftp = drupal_ftp_ftp_object();
     return FALSE;
   }
 
   if (!@ftp_systype($ftp->__conn)) {
-    // The connection is dead
+    // The connection is dead.
     return FALSE;
   }
   else {
-    // The connection is still alive
+    // The connection is still alive.
     return TRUE;
   }
 }
 
 /**
- *  This function tries to retrieve the contents of a file from the FTP server.
+ * This function tries to retrieve the contents of a file from the FTP server.
  *  Firstly it changes into the $directory directory, and then attempts to download the file $filename.
  *  The file is saved locally and its contents are returned to the caller of the function.
  */
 function drupal_ftp_ftp_to_file($file, $filename, $directory, &$ftp) {
   // Change into the remote directory and retrieve the content
-  // of a file. Once retrieve, return this value to the caller
-
+  // of a file. Once retrieve, return this value to the caller.
   if (!@drupal_ftp_connect($ftp)) {
     return FALSE;
   }
 
   // We are now connected, so let's retrieve the file contents.
-  // Firstly, we change into the directory
+  // Firstly, we change into the directory.
   $chdir = @ftp_chdir($ftp->__conn, $directory);
 
   if (!$chdir) {
@@ -218,7 +228,7 @@ function drupal_ftp_ftp_to_file($file, $filename, $directory, &$ftp) {
     return FALSE;
   }
 
-  // We have changed into the directory, let's attempt to get the file
+  // We have changed into the directory, let's attempt to get the file.
   $fp = @fopen($file, 'wb');
   $get_file = @ftp_fget($ftp->__conn, $fp, $filename, FTP_BINARY);
   fclose($fp);
@@ -229,7 +239,7 @@ function drupal_ftp_ftp_to_file($file, $filename, $directory, &$ftp) {
     _backup_migrate_message('FTP Error: Unable to download file: @filename from @directory', array('@filename' => $filename, '@directory' => $directory), 'error');
     return FALSE;
   }
-  
+
   return TRUE;
 }
 
@@ -242,15 +252,15 @@ function drupal_ftp_file_to_ftp($file, $ftp_filename, $ftp_directory, &$ftp) {
   }
 
   if ($source = drupal_realpath($file)) {
-    // Now we can try to write to the remote file
-    $complete_filename = $ftp_directory .'/'. $ftp_filename;
+    // Now we can try to write to the remote file.
+    $complete_filename = $ftp_directory . '/' . $ftp_filename;
     $put_file = @ftp_put($ftp->__conn, $complete_filename, $source, FTP_BINARY);
     if (!$put_file) {
       _backup_migrate_message('FTP Error: Couldn\'t write to @complete_filename when trying to save file on the ftp server.', array('@complete_filename' => $complete_filename), 'error');
       return FALSE;
     }
 
-    // Everything worked OK
+    // Everything worked OK.
     return TRUE;
   }
   else {
@@ -260,19 +270,18 @@ function drupal_ftp_file_to_ftp($file, $ftp_filename, $ftp_directory, &$ftp) {
 }
 
 /**
- *  The drupal_ftp_change_directory Function
+ * The drupal_ftp_change_directory Function
  *  This function simply changes into the $directory folder on the FTP server.
  *  If a connection or permission error occurs then _backup_migrate_message() will contain the error message.
  */
 function drupal_ftp_change_directory($directory, &$ftp) {
   // Switch to another directory on the web server. If we don't
-  // have permissions then an error will occur
-
+  // have permissions then an error will occur.
   if (!@drupal_ftp_connect($ftp)) {
     return FALSE;
   }
 
-  // Try and change into another directory
+  // Try and change into another directory.
   $chdir = ftp_chdir($ftp->__conn, $directory);
 
   if (!$chdir) {
@@ -280,21 +289,20 @@ function drupal_ftp_change_directory($directory, &$ftp) {
     return FALSE;
   }
   else {
-    // Changing directories worked OK
+    // Changing directories worked OK.
     return TRUE;
   }
 }
 
 /**
- *  The drupal_ftp_file_list Function
+ * The drupal_ftp_file_list Function
  *  This function will change into the $directory folder and get a list of files and directories contained in that folder.
  *  This function still needs a lot of work, but should work in most cases.
  */
 function drupal_ftp_file_list($directory, &$ftp) {
   // This function will attempt to change into the specified
   // directory and retrieve a list of files as an associative
-  // array. This list will include file name, size and date last modified
-
+  // array. This list will include file name, size and date last modified.
   $file_array = array();
 
   // Can we switch to the desired directory?
@@ -306,14 +314,14 @@ function drupal_ftp_file_list($directory, &$ftp) {
   // This is slower than parsing the raw return values, but it is faster.
   $file_list = ftp_nlist($ftp->__conn, $directory);
 
-  // Save the list of files
+  // Save the list of files.
   if (@is_array($file_list)) {
-    // Interate through the array
+    // Interate through the array.
     foreach ($file_list as $file) {
       $file_array[] = array(
         'filename' => $file,
-        'filesize' => ftp_size($ftp->__conn, $directory ."/". $file),
-        'filetime' => ftp_mdtm($ftp->__conn, $directory ."/". $file),
+        'filesize' => ftp_size($ftp->__conn, $directory . "/" . $file),
+        'filetime' => ftp_mdtm($ftp->__conn, $directory . "/" . $file),
       );
     }
   }
@@ -322,13 +330,12 @@ function drupal_ftp_file_list($directory, &$ftp) {
 }
 
 /**
- *  The drupal_ftp_create_directory Function
+ * The drupal_ftp_create_directory Function
  *  This function tries to make a new directory called $folder_name on the FTP server.
  *  If it can create the folder, then the folder is given appropriate rights with the CHMOD command.
  */
 function drupal_ftp_create_directory($folder_name, &$ftp) {
-  // Makes a new folder on the web server via FTP
-
+  // Makes a new folder on the web server via FTP.
   if (!@drupal_ftp_connect($ftp)) {
     return FALSE;
   }
@@ -337,7 +344,7 @@ function drupal_ftp_create_directory($folder_name, &$ftp) {
 
   if ($create_result == TRUE) {
     // Can we change the files permissions?
-    $exec_result = @ftp_site($ftp->__conn, 'chmod 0777 '. $folder_name .'/');
+    $exec_result = @ftp_site($ftp->__conn, 'chmod 0777 ' . $folder_name . '/');
 
     if ($exec_result == TRUE) {
       return TRUE;
@@ -354,11 +361,11 @@ function drupal_ftp_create_directory($folder_name, &$ftp) {
 }
 
 /**
- *  The drupal_ftp_delete_file Function
+ * The drupal_ftp_delete_file Function
  *  This function attempts to delete a file called $filename from the FTP server.
  */
 function drupal_ftp_delete_file($filename, &$ftp) {
-  // Remove the specified file from the FTP server
+  // Remove the specified file from the FTP server.
   if (!@drupal_ftp_connect($ftp)) {
     return FALSE;
   }
@@ -366,18 +373,18 @@ function drupal_ftp_delete_file($filename, &$ftp) {
   $delete_result = @ftp_delete($ftp->__conn, $filename);
 
   if ($delete_result == TRUE) {
-    // The file/folder was renamed successfully
+    // The file/folder was renamed successfully.
     return TRUE;
   }
   else {
-    // Couldn't delete the selected file
+    // Couldn't delete the selected file.
     _backup_migrate_message('FTP Error: Couldn\'t delete the selected file: @filename', array('@filename' => $filename), 'error');
     return FALSE;
   }
 }
 
 /**
- *  The drupal_ftp_delete_folder Function
+ * The drupal_ftp_delete_folder Function
  *  This function was one of the hardest to write. It recursively deletes all files and folders from a directory called $folder_name.
  */
 function drupal_ftp_delete_folder($folder_name, &$ftp) {
@@ -394,18 +401,18 @@ function drupal_ftp_delete_folder($folder_name, &$ftp) {
   $file_counter = 0;
   $content = @ftp_nlist($ftp->__conn, ".");
 
-  for ($i = 0; $i < sizeof($content); $i++) {
+  for ($i = 0; $i < count($content); $i++) {
     // If we can change into this then it's a directory.
-    // If not, it's a file
+    // If not, it's a file.
     if ($content[$i] != "." && $content[$i] != "..") {
       if (@ftp_chdir($ftp->__conn, $content[$i])) {
-        // We have a directory
+        // We have a directory.
         $directories[] = $content[$i];
         $dir_counter++;
         @ftp_cdup($ftp->__conn);
       }
       else {
-        // We have a file
+        // We have a file.
         $files[] = $content[$i];
         $file_counter++;
       }
@@ -417,10 +424,10 @@ function drupal_ftp_delete_folder($folder_name, &$ftp) {
   }
 
   for ($j = 0; $j < $dir_counter; $j++) {
-    if ($directories[$j] != "." OR $directories[$j] != "..") {
+    if ($directories[$j] != "." or $directories[$j] != "..") {
       $location = ftp_pwd($ftp->__conn);
       drupal_ftp_delete_folder($directories[$j], $ftp);
-      @ftp_cdup ($ftp->__conn);
+      @ftp_cdup($ftp->__conn);
       @ftp_rmdir($ftp->__conn, $directories[$j]);
     }
   }

File diff suppressed because it is too large
+ 534 - 266
sites/all/modules/contrib/admin/backup_migrate/includes/destinations.inc


+ 37 - 28
sites/all/modules/contrib/admin/backup_migrate/includes/destinations.s3.inc

@@ -1,6 +1,5 @@
 <?php
 
-
 /**
  * @file
  * Functions to handle the s3 backup destination.
@@ -12,15 +11,14 @@
  * @ingroup backup_migrate_destinations
  */
 class backup_migrate_destination_s3 extends backup_migrate_destination_remote {
-  var $supported_ops = array('scheduled backup', 'manual backup', 'restore', 'list files', 'configure', 'delete');
-  var $s3 = NULL;
-  var $cache_files = TRUE;
-
+  public $supported_ops = array('scheduled backup', 'manual backup', 'remote backup', 'restore', 'list files', 'configure', 'delete');
+  public $s3 = NULL;
+  public $cache_files = TRUE;
 
   /**
    * Save to to the s3 destination.
    */
-  function _save_file($file, $settings) {
+  public function _save_file($file, $settings) {
     if ($s3 = $this->s3_object()) {
       $path = $file->filename();
       if ($s3->putObject($s3->inputFile($file->filepath(), FALSE), $this->get_bucket(), $this->remote_path($file->filename()), S3::ACL_PRIVATE)) {
@@ -33,8 +31,9 @@ class backup_migrate_destination_s3 extends backup_migrate_destination_remote {
   /**
    * Load from the s3 destination.
    */
-  function load_file($file_id) {
-    backup_migrate_include('files');
+  public function load_file($file_id) {
+    require_once dirname(__FILE__) . '/files.inc';
+
     $file = new backup_file(array('filename' => $file_id));
     if ($s3 = $this->s3_object()) {
       $data = $s3->getObject($this->get_bucket(), $this->remote_path($file_id), $file->filepath());
@@ -42,13 +41,14 @@ class backup_migrate_destination_s3 extends backup_migrate_destination_remote {
         return $file;
       }
     }
+
     return NULL;
   }
 
   /**
    * Delete from the s3 destination.
    */
-  function _delete_file($file_id) {
+  public function _delete_file($file_id) {
     if ($s3 = $this->s3_object()) {
       $s3->deleteObject($this->get_bucket(), $this->remote_path($file_id));
     }
@@ -57,12 +57,13 @@ class backup_migrate_destination_s3 extends backup_migrate_destination_remote {
   /**
    * List all files from the s3 destination.
    */
-  function _list_files() {
-    backup_migrate_include('files');
+  public function _list_files() {
+    require_once dirname(__FILE__) . '/files.inc';
+
     $files = array();
     if ($s3 = $this->s3_object()) {
       $s3_files = $s3->getBucket($this->get_bucket(), $this->get_subdir());
-      foreach ((array)$s3_files as $id => $file) {
+      foreach ((array) $s3_files as $id => $file) {
         $info = array(
           'filename' => $this->local_path($file['name']),
           'filesize' => $file['size'],
@@ -71,21 +72,21 @@ class backup_migrate_destination_s3 extends backup_migrate_destination_remote {
         $files[$info['filename']] = new backup_file($info);
       }
     }
+
     return $files;
   }
 
   /**
    * Get the form for the settings for this filter.
    */
-  function edit_form() {
+  public function edit_form() {
     // Check for the library.
     $this->s3_object();
 
     $form = parent::edit_form();
     $form['scheme']['#type'] = 'value';
     $form['scheme']['#value'] = 'https';
-    $form['host']['#type'] = 'value';
-    $form['host']['#value'] = 's3.amazonaws.com';
+    $form['host']['#default_value'] = @$this->dest_url['host'] ? $this->dest_url['host'] : 's3.amazonaws.com';
 
     $form['path']['#title'] = 'S3 Bucket';
     $form['path']['#default_value'] = $this->get_bucket();
@@ -98,7 +99,7 @@ class backup_migrate_destination_s3 extends backup_migrate_destination_remote {
       '#type' => 'textfield',
       '#title' => t('Subdirectory'),
       '#default_value' => $this->get_subdir(),
-      '#weight' => 25
+      '#weight' => 25,
     );
     $form['settings']['#weight'] = 50;
 
@@ -108,11 +109,10 @@ class backup_migrate_destination_s3 extends backup_migrate_destination_remote {
   /**
    * Submit the form for the settings for the s3 destination.
    */
-  function edit_form_submit($form, &$form_state) {
+  public function edit_form_submit($form, &$form_state) {
     // Append the subdir onto the path.
-
     if (!empty($form_state['values']['subdir'])) {
-      $form_state['values']['path'] .= '/'. trim($form_state['values']['subdir'], '/');
+      $form_state['values']['path'] .= '/' . trim($form_state['values']['subdir'], '/');
     }
     parent::edit_form_submit($form, $form_state);
   }
@@ -120,9 +120,9 @@ class backup_migrate_destination_s3 extends backup_migrate_destination_remote {
   /**
    * Generate a filepath with the correct prefix.
    */
-  function remote_path($path) {
+  public function remote_path($path) {
     if ($subdir = $this->get_subdir()) {
-      $path = $subdir .'/'. $path;
+      $path = $subdir . '/' . $path;
     }
     return $path;
   }
@@ -130,9 +130,9 @@ class backup_migrate_destination_s3 extends backup_migrate_destination_remote {
   /**
    * Generate a filepath with the correct prefix.
    */
-  function local_path($path) {
+  public function local_path($path) {
     if ($subdir = $this->get_subdir()) {
-      $path = str_replace($subdir .'/', '', $path);
+      $path = str_replace($subdir . '/', '', $path);
     }
     return $path;
   }
@@ -140,7 +140,7 @@ class backup_migrate_destination_s3 extends backup_migrate_destination_remote {
   /**
    * Get the bucket which is the first part of the path.
    */
-  function get_bucket() {
+  public function get_bucket() {
     $parts = explode('/', @$this->dest_url['path']);
     return $parts[0];
   }
@@ -148,7 +148,7 @@ class backup_migrate_destination_s3 extends backup_migrate_destination_remote {
   /**
    * Get the bucket which is the first part of the path.
    */
-  function get_subdir() {
+  public function get_subdir() {
     // Support the older style of subdir saving.
     if ($subdir = $this->settings('subdir')) {
       return $subdir;
@@ -158,7 +158,10 @@ class backup_migrate_destination_s3 extends backup_migrate_destination_remote {
     return implode('/', array_filter($parts));
   }
 
-  function s3_object() {
+  /**
+   *
+   */
+  public function s3_object() {
     // Try to use libraries module if available to find the path.
     if (function_exists('libraries_get_path')) {
       $library_paths[] = libraries_get_path('s3-php5-curl');
@@ -169,11 +172,16 @@ class backup_migrate_destination_s3 extends backup_migrate_destination_remote {
     $library_paths[] = drupal_get_path('module', 'backup_migrate') . '/includes/s3-php5-curl';
     $library_paths[] = drupal_get_path('module', 'backup_migrate') . '/includes';
 
-    foreach($library_paths as $path) {
+    foreach ($library_paths as $path) {
       if (file_exists($path . '/S3.php')) {
         require_once $path . '/S3.php';
         if (!$this->s3 && !empty($this->dest_url['user'])) {
-          $this->s3 = new S3($this->dest_url['user'], $this->dest_url['pass']);
+          // The hostname can be overridden.
+          $host = 's3.amazonaws.com';
+          if (isset($this->dest_url['host'])) {
+            $host = $this->dest_url['host'];
+          }
+          $this->s3 = new S3($this->dest_url['user'], $this->dest_url['pass'], FALSE, $host);
         }
         return $this->s3;
       }
@@ -181,4 +189,5 @@ class backup_migrate_destination_s3 extends backup_migrate_destination_remote {
     drupal_set_message(t('Due to drupal.org code hosting policies, the S3 library needed to use an S3 destination is no longer distributed with this module. You must download the library from !link and place it in one of these locations: %locations.', array('%locations' => implode(', ', $library_paths), '!link' => l('http://undesigned.org.za/2007/10/22/amazon-s3-php-class', 'http://undesigned.org.za/2007/10/22/amazon-s3-php-class'))), 'error', FALSE);
     return NULL;
   }
+
 }

+ 185 - 64
sites/all/modules/contrib/admin/backup_migrate/includes/files.inc

@@ -1,6 +1,5 @@
 <?php
 
-
 /**
  * @file
  * General file handling code for Backup and Migrate.
@@ -12,7 +11,7 @@ define('BACKUP_MIGRATE_FILENAME_MAXLENGTH', 255);
  * Add a file to the temporary files list for deletion when we're done.
  */
 function backup_migrate_temp_files_add($filepath = NULL) {
-  static $files = array();
+  $files = &drupal_static('backup_migrate_temp_files_add', array());
   if (!$filepath) {
     return $files;
   }
@@ -25,41 +24,104 @@ function backup_migrate_temp_files_add($filepath = NULL) {
  * Delete all temporary files.
  */
 function _backup_migrate_temp_files_delete() {
-  // Delete the temp files created during this run.
-  foreach (backup_migrate_temp_files_add() as $file) {
-    $file = drupal_realpath($file);
-    if (file_exists($file) && is_writable($file)) {
+  if (variable_get('backup_migrate_cleanup_temp_files', BACKUP_MIGRATE_CLEANUP_TEMP_FILES)) {
+    // Delete the temp files created during this run.
+    foreach (backup_migrate_temp_files_add() as $file) {
+      if (file_exists($file) && is_writable($file)) {
+        _backup_migrate_temp_files_delete_file($file);
+      }
+    }
+
+    // Delete temp files abandoned for 6 or more hours.
+    $dir = file_directory_temp();
+    $expire = time() - variable_get('backup_migrate_cleanup_time', BACKUP_MIGRATE_CLEANUP_TIME);
+    if (file_exists($dir) && is_dir($dir) && is_readable($dir) && $handle = opendir($dir)) {
+      while (FALSE !== ($file = @readdir($handle))) {
+        // Delete 'backup_migrate_' files in the temp directory that are older than the expire time.
+        // We should only attempt to delete writable files to prevent errors in shared environments.
+        // This could still cause issues in shared environments with poorly configured file permissions.
+        if (strpos($file, 'backup_migrate_') === 0 && is_writable("$dir/$file") && @filectime("$dir/$file") < $expire) {
+          _backup_migrate_temp_files_delete_file("$dir/$file");
+        }
+      }
+      closedir($handle);
+    }
+  }
+}
+
+/**
+ * Delete a temporary file or folder.
+ */
+function _backup_migrate_temp_files_delete_file($file) {
+  if (file_exists($file) && (is_writable($file) || is_link($file))) {
+    if (!is_link($file) && is_dir($file) && is_readable($file) && $handle = opendir($file)) {
+      $dir = $file;
+      while (FALSE !== ($file = @readdir($handle))) {
+        if ($file != '..' && $file != '.') {
+          _backup_migrate_temp_files_delete_file("$dir/$file");
+        }
+      }
+      closedir($handle);
+      rmdir($dir);
+    }
+    else {
       unlink($file);
     }
   }
+}
 
-  // Delete temp files abandoned for 6 or more hours.
-  $dir = file_stream_wrapper_get_instance_by_scheme('temporary')->getDirectoryPath();
-  $expire = time() - variable_get('backup_migrate_cleanup_time', 21600);
-  if (file_exists($dir) && is_dir($dir) && is_readable($dir) && $handle = opendir($dir)) {
-    while (FALSE !== ($file = @readdir($handle))) {
-      // Delete 'backup_migrate_' files in the temp directory that are older than the expire time.
-      // We should only attempt to delete writable files to prevent errors in shared environments.
-      // This could still cause issues in shared environments with poorly configured file permissions.
-      if (strpos($file, 'backup_migrate_') === 0 && is_writable("$dir/$file") && @filectime("$dir/$file") < $expire) {
-        unlink("$dir/$file");
+/**
+ * Move files recursively.
+ */
+function _backup_migrate_move_files($from, $to) {
+  if (is_readable($from)) {
+    if (is_dir($from) && is_readable($from) && $handle = opendir($from)) {
+      if (!file_exists($to)) {
+        mkdir($to);
       }
+      while (FALSE !== ($file = @readdir($handle))) {
+        if ($file != '..' && $file != '.') {
+          _backup_migrate_move_files("$from/$file", "$to/$file");
+        }
+      }
+      closedir($handle);
+    }
+    else {
+      rename($from, $to);
     }
-    closedir($handle);
   }
+  return FALSE;
+}
+
+/**
+ * Create a temporary directory.
+ */
+function backup_migrate_temp_directory() {
+  $tmp = realpath(file_directory_temp());
+  // Check the writability of the temp directory.
+  if (!is_writable(realpath(file_directory_temp()))) {
+    _backup_migrate_message('Your temporary directory %tmp is not writable. Backup and migrate needs to be able to create temporary files.', array('%tmp' => $tmp), 'error');
+  }
+
+  // Use a full path so that the files can be deleted during the shutdown function if needed.
+  $file = $tmp . '/' . uniqid('backup_migrate_');
+  mkdir($file);
+  backup_migrate_temp_files_add($file);
+  return $file;
 }
 
 /**
  * Return a list of backup filetypes.
  */
 function _backup_migrate_filetypes() {
-  backup_migrate_include('filters');
+  require_once dirname(__FILE__) . '/filters.inc';
 
   $out = backup_migrate_filters_file_types();
 
   foreach ($out as $key => $info) {
     $out[$key]['id'] = empty($info['id']) ? $key : $info['id'];
   }
+
   return $out;
 }
 
@@ -78,19 +140,33 @@ function _backup_migrate_filename_append_prepare($filename, $append_str) {
 /**
  * Construct a filename using token and some cleaning.
  */
-function _backup_migrate_construct_filename($filename, $timestamp='') {
-  if (module_exists('token')) {
+function _backup_migrate_construct_filename($settings) {
+  // Get the raw filename from the settings.
+  $filename = $settings->filename;
+
+  // Replace any tokens.
+  if (module_exists('token') && function_exists('token_replace')) {
     $filename = token_replace($filename);
   }
-  $filename = preg_replace("/[^a-zA-Z0-9\.\-_]/", "", $filename);
+
+  // Remove illegal characters.
+  $filename = _backup_migrate_clean_filename($filename);
+
+  // Generate a timestamp if needed.
+  $timestamp = '';
+  if ($settings->append_timestamp == 1 && $settings->timestamp_format) {
+    $timestamp = format_date(time(), 'custom', $settings->timestamp_format);
+  }
+
+  // Trim to length if needed to allow the timestamp to fit into the max filename.
   $filename = _backup_migrate_filename_append_prepare($filename, $timestamp);
   $filename .= '-' . $timestamp;
   $filename = trim($filename, '-');
 
+  // If there is no filename, then just call it untitled.
   if (drupal_strlen($filename) == 0) {
     $filename = 'untitled';
   }
-
   return $filename;
 }
 
@@ -102,11 +178,20 @@ function _backup_migrate_default_filename() {
     return '[site:name]';
   }
   else {
-    // Cleaning the string isn't strictly necessary but it looks better in the settings field.
-    return variable_get('site_name', 'backup_migrate');
+    // Cleaning the string isn't strictly necessary but it looks better in the
+    // settings field.
+    return _backup_migrate_clean_filename(variable_get('site_name', "backup_migrate"));
   }
 }
 
+/**
+ * Clean up a filename to remove unsafe characters.
+ */
+function _backup_migrate_clean_filename($filename) {
+  $filename = preg_replace("/[^a-zA-Z0-9\.\-_]/", "", $filename);
+  return $filename;
+}
+
 /**
  * An output buffer callback which simply throws away the buffer instead of sending it to the browser.
  */
@@ -114,22 +199,21 @@ function _backup_migrate_file_dispose_buffer($buffer) {
   return "";
 }
 
-
 /**
  * A backup file which allows for saving to and reading from the server.
  */
 class backup_file {
-  var $file_info = array();
-  var $type = array();
-  var $ext = array();
-  var $path = "";
-  var $name = "";
-  var $handle = NULL;
+  public $file_info = array();
+  public $type = array();
+  public $ext = array();
+  public $path = "";
+  public $name = "";
+  public $handle = NULL;
 
   /**
    * Construct a file object given a file path, or create a temp file for writing.
    */
-  function backup_file($params = array()) {
+  public function __construct($params = array()) {
     if (isset($params['filepath']) && file_exists($params['filepath'])) {
       $this->set_filepath($params['filepath']);
     }
@@ -142,7 +226,7 @@ class backup_file {
   /**
    * Get the file_id if the file has been saved to a destination.
    */
-  function file_id() {
+  public function file_id() {
     // The default file_id is the filename. Destinations can override the file_id if needed.
     return isset($this->file_info['file_id']) ? $this->file_info['file_id'] : $this->filename();
   }
@@ -150,27 +234,33 @@ class backup_file {
   /**
    * Get the current filepath.
    */
-  function filepath() {
-    return drupal_realpath($this->path);
+  public function filepath() {
+    if ($filepath = drupal_realpath($this->path)) {
+      return $filepath;
+    }
+    return $this->path;
   }
 
   /**
    * Get the final filename.
    */
-  function filename($name = NULL) {
+  public function filename($name = NULL) {
     if ($name) {
       $this->name = $name;
     }
-    return $this->name .'.'. $this->extension();
+    $extension_str = '.' . $this->extension();
+    $this->name = _backup_migrate_filename_append_prepare($this->name, $extension_str);
+    return $this->name . $extension_str;
   }
 
   /**
    * Set the current filepath.
    */
-  function set_filepath($path) {
+  public function set_filepath($path) {
     $this->path = $path;
     $params = array(
       'filename' => basename($path),
+      'file_id' => basename($path),
     );
     if (file_exists($path)) {
       $params['filesize'] = filesize($path);
@@ -182,67 +272,93 @@ class backup_file {
   /**
    * Get one or all pieces of info for the file.
    */
-  function info($key = NULL) {
+  public function info($key = NULL) {
     if ($key) {
       return @$this->file_info[$key];
     }
     return $this->file_info;
   }
 
+  /**
+   * Get one or all pieces of info for the file.
+   */
+  public function info_set($key, $value) {
+    $this->file_info[$key] = $value;
+  }
+
   /**
    * Get the file extension.
    */
-  function extension() {
+  public function extension() {
     return implode(".", $this->ext);
   }
 
   /**
    * Get the file type.
    */
-  function type() {
+  public function type() {
     return $this->type;
   }
 
   /**
    * Get the file mimetype.
    */
-  function mimetype() {
+  public function mimetype() {
     return @$this->type['filemime'] ? $this->type['filemime'] : 'application/octet-stream';
   }
 
   /**
    * Get the file mimetype.
    */
-  function type_id() {
+  public function type_id() {
     return @$this->type['id'];
   }
 
+  /**
+   *
+   */
+  public function filesize() {
+    if (empty($this->file_info['filesize'])) {
+      $this->calculate_filesize();
+    }
+    return $this->file_info['filesize'];
+  }
+
+  /**
+   *
+   */
+  public function calculate_filesize() {
+    $this->file_info['filesize'] = '';
+    if (!empty($this->path) && file_exists($this->path)) {
+      $this->file_info['filesize'] = filesize($this->path);
+    }
+  }
 
   /**
    * Can this file be used to backup to.
    */
-  function can_backup() {
+  public function can_backup() {
     return @$this->type['backup'];
   }
 
   /**
    * Can this file be used to restore to.
    */
-  function can_restore() {
+  public function can_restore() {
     return @$this->type['restore'];
   }
 
   /**
    * Can this file be used to restore to.
    */
-  function is_recognized_type() {
+  public function is_recognized_type() {
     return @$this->type['restore'] || @$this->type['backup'];
   }
 
   /**
    * Open a file for reading or writing.
    */
-  function open($write = FALSE, $binary = FALSE) {
+  public function open($write = FALSE, $binary = FALSE) {
     if (!$this->handle) {
       $path = $this->filepath();
 
@@ -267,7 +383,7 @@ class backup_file {
   /**
    * Close a file when we're done reading/writing.
    */
-  function close() {
+  public function close() {
     fclose($this->handle);
     $this->handle = NULL;
   }
@@ -275,7 +391,7 @@ class backup_file {
   /**
    * Write a line to the file.
    */
-  function write($data) {
+  public function write($data) {
     if (!$this->handle) {
       $this->handle = $this->open(TRUE);
     }
@@ -287,7 +403,7 @@ class backup_file {
   /**
    * Read a line from the file.
    */
-  function read($size = NULL) {
+  public function read($size = NULL) {
     if (!$this->handle) {
       $this->handle = $this->open();
     }
@@ -300,14 +416,14 @@ class backup_file {
   /**
    * Write data to the file.
    */
-  function put_contents($data) {
+  public function put_contents($data) {
     file_put_contents($this->filepath(), $data);
   }
 
   /**
    * Read data from the file.
    */
-  function get_contents() {
+  public function get_contents() {
     return file_get_contents($this->filepath());
   }
 
@@ -315,11 +431,16 @@ class backup_file {
    * Transfer file using http to client. Similar to the built in file_transfer,
    *  but it calls module_invoke_all('exit') so that temp files can be deleted.
    */
-  function transfer() {
+  public function transfer() {
     $headers = array(
       array('key' => 'Content-Type', 'value' => $this->mimetype()),
-      array('key' => 'Content-Disposition', 'value' => 'attachment; filename="'. $this->filename() .'"'),
+      array('key' => 'Content-Disposition', 'value' => 'attachment; filename="' . $this->filename() . '"'),
     );
+    // In some circumstances, web-servers will double compress gzipped files.
+    // This may help aleviate that issue by disabling mod-deflate.
+    if ($this->mimetype() == 'application/x-gzip') {
+      $headers[] = array('key' => 'Content-Encoding', 'value' => 'gzip');
+    }
     if ($size = $this->info('filesize')) {
       $headers[] = array('key' => 'Content-Length', 'value' => $size);
     }
@@ -358,7 +479,7 @@ class backup_file {
   /**
    * Push a file extension onto the file and return the previous file path.
    */
-  function push_type($extension) {
+  public function push_type($extension) {
     $types = _backup_migrate_filetypes();
     if ($type = @$types[$extension]) {
       $this->push_filetype($type);
@@ -372,7 +493,7 @@ class backup_file {
   /**
    * Push a file extension onto the file and return the previous file path.
    */
-  function pop_type() {
+  public function pop_type() {
     $out = new backup_file(array('filepath' => $this->filepath()));
     $this->pop_filetype();
     $this->temporary_file();
@@ -382,7 +503,7 @@ class backup_file {
   /**
    * Set the current file type.
    */
-  function set_filetype($type) {
+  public function set_filetype($type) {
     $this->type = $type;
     $this->ext = array($type['extension']);
   }
@@ -390,7 +511,7 @@ class backup_file {
   /**
    * Set the current file type.
    */
-  function push_filetype($type) {
+  public function push_filetype($type) {
     $this->ext[] = $type['extension'];
     $this->type = $type;
   }
@@ -398,7 +519,7 @@ class backup_file {
   /**
    * Pop the current file type.
    */
-  function pop_filetype() {
+  public function pop_filetype() {
     array_pop($this->ext);
     $this->detect_filetype_from_extension();
   }
@@ -406,7 +527,7 @@ class backup_file {
   /**
    * Set the file info.
    */
-  function set_file_info($file_info) {
+  public function set_file_info($file_info) {
     $this->file_info = $file_info;
 
     $this->ext = explode('.', @$this->file_info['filename']);
@@ -421,7 +542,7 @@ class backup_file {
   /**
    * Get the filetype info of the given file, or false if the file is not a valid type.
    */
-  function detect_filetype_from_extension() {
+  public function detect_filetype_from_extension() {
     $ext = end($this->ext);
     $this->type = array();
     $types = _backup_migrate_filetypes();
@@ -436,17 +557,17 @@ class backup_file {
   /**
    * Get a temporary file name with path.
    */
-  function temporary_file() {
+  public function temporary_file() {
     $file = drupal_tempnam('temporary://', 'backup_migrate_');
     // Add the version without the extension. The tempnam function creates this for us.
     backup_migrate_temp_files_add($file);
 
     if ($this->extension()) {
-      $file .= '.'. $this->extension();
+      $file .= '.' . $this->extension();
       // Add the version with the extension. This is the one we will actually use.
       backup_migrate_temp_files_add($file);
     }
     $this->path = $file;
   }
-}
 
+}

+ 131 - 37
sites/all/modules/contrib/admin/backup_migrate/includes/filters.backup_restore.inc

@@ -1,9 +1,10 @@
 <?php
 
-
 /**
  * @file
- * This filter performs tha actual backup or restore operation. Not technically a filter per-se, but it does need to fit in the call chain.
+ * This filter performs tha actual backup or restore operation.
+ *
+ * Not technically a filter per-se, but it does need to fit in the call chain.
  */
 
 /**
@@ -12,12 +13,12 @@
  * @ingroup backup_migrate_filters
  */
 class backup_migrate_filter_backup_restore extends backup_migrate_filter {
-  var $op_weights = array('backup' => 0, 'restore' => 0);
+  public $op_weights = array('backup' => 0, 'restore' => 0);
 
   /**
    * Get the default destinations for this filter.
    */
-  function destinations() {
+  public function destinations() {
     $out = array();
     foreach ($this->_get_destination_types() as $destination) {
       if (method_exists($destination, 'destinations')) {
@@ -27,15 +28,28 @@ class backup_migrate_filter_backup_restore extends backup_migrate_filter {
     return $out;
   }
 
+  /**
+   * Get the default sources for this filter.
+   */
+  public function sources() {
+    $out = array();
+    foreach ($this->_get_source_types() as $type) {
+      if (method_exists($type, 'sources')) {
+        $out += $type->sources();
+      }
+    }
+    return $out;
+  }
 
   /**
    * Get the default backup settings for this filter.
    */
-  function backup_settings_default() {
-    backup_migrate_include('destinations');
+  public function backup_settings_default() {
+    require_once dirname(__FILE__) . '/sources.inc';
+
     $out = array();
-    foreach (backup_migrate_get_destinations('source') as $destination) {
-      $out['destinations'][$destination->get_id()] = $destination->backup_settings_default();
+    foreach (backup_migrate_get_sources() as $source) {
+      $out['sources'][$source->get_id()] = $source->backup_settings_default();
     }
     return $out;
   }
@@ -43,7 +57,7 @@ class backup_migrate_filter_backup_restore extends backup_migrate_filter {
   /**
    * Get the form for the settings for this filter.
    */
-  function backup_settings_form_validate($form, &$form_state) {
+  public function backup_settings_form_validate($form, &$form_state) {
     foreach ($this->_get_destination_types() as $destination) {
       $destination->backup_settings_form_validate($form, $form_state);
     }
@@ -52,7 +66,7 @@ class backup_migrate_filter_backup_restore extends backup_migrate_filter {
   /**
    * Submit the settings form. Any values returned will be saved.
    */
-  function backup_settings_form_submit($form, &$form_state) {
+  public function backup_settings_form_submit($form, &$form_state) {
     foreach ($this->_get_destination_types() as $destination) {
       $destination->backup_settings_form_submit($form, $form_state);
     }
@@ -61,7 +75,7 @@ class backup_migrate_filter_backup_restore extends backup_migrate_filter {
   /**
    * Get the default restore settings for this filter.
    */
-  function restore_settings_default() {
+  public function restore_settings_default() {
     $out = array();
     foreach ($this->_get_destination_types() as $destination) {
       $out += $destination->restore_settings_default();
@@ -72,22 +86,24 @@ class backup_migrate_filter_backup_restore extends backup_migrate_filter {
   /**
    * Get the form for the backup settings for this filter.
    */
-  function backup_settings_form($settings) {
-    backup_migrate_include('destinations');
-    $out = array('destinations' => array(
-      '#tree' => TRUE,
-      
-    ));
-    foreach (backup_migrate_get_destinations('source') as $destination) {
-      $destination_settings = (array)(@$settings['destinations'][$destination->get_id()]) + $settings;
-      if ($form = $destination->backup_settings_form($destination_settings)) {
-        $out['destinations'][$destination->get_id()] = array(
+  public function backup_settings_form($settings) {
+    require_once dirname(__FILE__) . '/sources.inc';
+
+    $out = array(
+      'sources' => array(
+        '#tree' => TRUE,
+      ),
+    );
+    foreach (backup_migrate_get_sources() as $source) {
+      $source_settings = (array) (@$settings['sources'][$source->get_id()]) + $settings;
+      if ($form = $source->backup_settings_form($source_settings)) {
+        $out['sources'][$source->get_id()] = array(
           '#type' => 'fieldset',
-          '#title' => t('!name Backup Options', array('!name' => $destination->get('name'))),
+          '#title' => t('!name Backup Options', array('!name' => $source->get('name'))),
           "#collapsible" => TRUE,
           "#collapsed" => TRUE,
           '#tree' => TRUE,
-          '#parents' => array('filters', 'destinations', $destination->get_id()),
+          '#parents' => array('filters', 'sources', $source->get_id()),
         ) + $form;
       }
     }
@@ -97,7 +113,7 @@ class backup_migrate_filter_backup_restore extends backup_migrate_filter {
   /**
    * Get the form for the restore settings for this filter.
    */
-  function restore_settings_form($settings) {
+  public function restore_settings_form($settings) {
     $form = array();
     foreach ($this->_get_destination_types() as $destination) {
       $destination->restore_settings_form($form, $settings);
@@ -105,25 +121,72 @@ class backup_migrate_filter_backup_restore extends backup_migrate_filter {
     return $form;
   }
 
+  /**
+   * Get the before-backup form for the active sources and destinations.
+   */
+  public function before_action_form($op, $settings) {
+    $form = array();
+    $method = 'before_' . $op . '_form';
+    if ($source = $settings->get_source()) {
+      if (method_exists($source, $method)) {
+        $form += $source->{$method}($settings);
+      }
+    }
+    foreach ($settings->get_destinations() as $destination) {
+      if (method_exists($destination, $method)) {
+        $form += $destination->{$method}($settings);
+      }
+    }
+    return $form;
+  }
+
+  /**
+   * Get the before-backup form for the active sources and destinations.
+   */
+  public function before_action_form_validate($op, $settings, $form, $form_state) {
+    $method = 'before_' . $op . '_form_validate';
+    foreach ($settings->get_all_locations() as $location) {
+      if (method_exists($location, $method)) {
+        $location->{$method}($settings, $form, $form_state);
+      }
+    }
+  }
+
+  /**
+   * Get the before-backup form for the active sources and destinations.
+   */
+  public function before_action_form_submit($op, $settings, $form, $form_state) {
+    $method = 'before_' . $op . '_form_submit';
+    foreach ($settings->get_all_locations() as $location) {
+      if (method_exists($location, $method)) {
+        $location->{$method}($settings, $form, $form_state);
+      }
+    }
+  }
+
   /**
    * Get the file types supported by this destination.
    */
-  function file_types() {
+  public function file_types() {
     $types = array();
     foreach ($this->_get_destination_types() as $destination) {
       $types += $destination->file_types();
     }
+    foreach ($this->_get_source_types() as $source) {
+      $types += $source->file_types();
+    }
     return $types;
   }
 
   /**
    * Backup the data from the source specified in the settings.
    */
-  function backup($file, &$settings) {
+  public function backup($file, $settings) {
     if ($source = $settings->get_source()) {
-      if (!empty($settings->filters['destinations'][$source->get_id()])) {
-        $settings->filters = (array)($settings->filters['destinations'][$source->get_id()]) + $settings->filters;
+      if (!empty($settings->filters['sources'][$source->get_id()])) {
+        $settings->filters = (array) ($settings->filters['sources'][$source->get_id()]) + $settings->filters;
       }
+
       $file = $source->backup_to_file($file, $settings);
       return $file;
     }
@@ -134,10 +197,10 @@ class backup_migrate_filter_backup_restore extends backup_migrate_filter {
   /**
    * Restore the data from to source specified in the settings.
    */
-  function restore($file, &$settings) {
+  public function restore($file, $settings) {
     if ($source = $settings->get_source()) {
-      if (!empty($settings->filters['destinations'][$source->get_id()])) {
-        $settings->filters = (array)($settings->filters['destinations'][$source->get_id()]) + $settings->filters;
+      if (!empty($settings->filters['sources'][$source->get_id()])) {
+        $settings->filters = (array) ($settings->filters['sources'][$source->get_id()]) + $settings->filters;
       }
       $num = $source->restore_from_file($file, $settings);
       return $num ? $file : FALSE;
@@ -147,23 +210,54 @@ class backup_migrate_filter_backup_restore extends backup_migrate_filter {
   }
 
   /**
-   * Get a list of dummy destinations representing each of the available destination types.
+   * Dummy destinations representing each of the available destination types.
+   *
+   * @return array
+   *   All of the available destination types.
    */
-  function _get_destination_types() {
-    backup_migrate_include('destinations');
-    static $destinations = NULL;
+  public function _get_destination_types() {
+    require_once dirname(__FILE__) . '/destinations.inc';
+
+    $destinations = &drupal_static('backup_migrate_filter_backup_restore::_get_destination_types', NULL);
     if (!is_array($destinations)) {
       $destinations = array();
-      $types = backup_migrate_get_destination_types();
+      $types = backup_migrate_get_destination_subtypes();
       // If no (valid) node type has been provided, display a node type overview.
       foreach ($types as $key => $type) {
         // Include the necessary file if specified by the type.
         if (!empty($type['file'])) {
-          require_once './'. $type['file'];
+          require_once './' . $type['file'];
         }
         $destinations[] = new $type['class'](array());
       }
     }
     return $destinations;
   }
+
+  /**
+   * Dummy destinations representing each of the available source types.
+   *
+   * @return array
+   *   All of the available source types.
+   */
+  public function _get_source_types() {
+    require_once dirname(__FILE__) . '/sources.inc';
+
+    $sources = &drupal_static('backup_migrate_filter_backup_restore::_get_source_types', NULL);
+    if (!is_array($sources)) {
+      $sources = array();
+      $types = backup_migrate_get_source_subtypes();
+      // If no (valid) node type has been provided, display a node type
+      // overview.
+      foreach ($types as $key => $type) {
+        // Include the necessary file if specified by the type.
+        if (!empty($type['file'])) {
+          require_once './' . $type['file'];
+        }
+        $sources[] = new $type['class'](array());
+      }
+    }
+    return $sources;
+  }
+
 }

+ 33 - 26
sites/all/modules/contrib/admin/backup_migrate/includes/filters.compression.inc

@@ -1,6 +1,5 @@
 <?php
 
-
 /**
  * @file
  * A filter for compressing bckups with zip, gz bzip etc.
@@ -12,26 +11,26 @@
  * @ingroup backup_migrate_filters
  */
 class backup_migrate_filter_compression extends backup_migrate_filter {
-  var $op_weights = array('backup' => 100, 'restore' => -100);
+  public $op_weights = array('backup' => 100, 'restore' => -100);
 
   /**
    * This function is called on a backup file after the backup has been completed.
    */
-  function backup($file, &$settings) {
+  public function backup($file, $settings) {
     return $this->_backup_migrate_file_compress($file, $settings);
   }
 
   /**
    * This function is called on a backup file before importing it.
    */
-  function restore($file, &$settings) {
-    return $this->_backup_migrate_file_decompress($file);
+  public function restore($file, $settings) {
+    return $this->_backup_migrate_file_decompress($file, $settings);
   }
 
   /**
    * Get the form for the settings for this filter.
    */
-  function backup_settings_default() {
+  public function backup_settings_default() {
     $options = $this->_backup_migrate_get_compression_form_item_options();
     return array('compression' => isset($options['gzip']) ? 'gzip' : 'none');
   }
@@ -39,7 +38,7 @@ class backup_migrate_filter_compression extends backup_migrate_filter {
   /**
    * Get the form for the settings for this filter.
    */
-  function backup_settings_form($settings) {
+  public function backup_settings_form($settings) {
     $form = array();
     $compression_options = $this->_backup_migrate_get_compression_form_item_options();
     $form['file']['compression'] = array(
@@ -54,7 +53,7 @@ class backup_migrate_filter_compression extends backup_migrate_filter {
   /**
    * Return a list of backup filetypes.
    */
-  function file_types() {
+  public function file_types() {
     return array(
       "gzip" => array(
         "extension" => "gz",
@@ -86,7 +85,7 @@ class backup_migrate_filter_compression extends backup_migrate_filter {
   /**
    * Get the compression options as an options array for a form item.
    */
-  function _backup_migrate_get_compression_form_item_options() {
+  public function _backup_migrate_get_compression_form_item_options() {
     $compression_options = array("none" => t("No Compression"));
     if (@function_exists("gzencode")) {
       $compression_options['gzip'] = t("GZip");
@@ -103,10 +102,14 @@ class backup_migrate_filter_compression extends backup_migrate_filter {
   /**
    * Gzip encode a file.
    */
-  function _backup_migrate_gzip_encode($source, $dest, $level = 9) {
+  public function _backup_migrate_gzip_encode($source, $dest, $level = 9, $settings) {
     $success = FALSE;
-    if (@function_exists("gzopen")) {
-      if (($fp_out = gzopen($dest, 'wb'. $level)) && ($fp_in = fopen($source, 'rb'))) {
+    // Try command line gzip first.
+    if (!empty($settings->filters['use_cli'])) {
+      $success = backup_migrate_exec("gzip -c -$level %input > %dest", array('%input' => $source, '%dest' => $dest, '%level' => $level));
+    }
+    if (!$success && @function_exists("gzopen")) {
+      if (($fp_out = gzopen($dest, 'wb' . $level)) && ($fp_in = fopen($source, 'rb'))) {
         while (!feof($fp_in)) {
           gzwrite($fp_out, fread($fp_in, 1024 * 512));
         }
@@ -121,9 +124,14 @@ class backup_migrate_filter_compression extends backup_migrate_filter {
   /**
    * Gzip decode a file.
    */
-  function _backup_migrate_gzip_decode($source, $dest) {
+  public function _backup_migrate_gzip_decode($source, $dest, $settings) {
     $success = FALSE;
-    if (@function_exists("gzopen")) {
+
+    if (!empty($settings->filters['use_cli'])) {
+      $success = backup_migrate_exec("gzip -d -c %input > %dest", array('%input' => $source, '%dest' => $dest));
+    }
+
+    if (!$success && @function_exists("gzopen")) {
       if (($fp_out = fopen($dest, 'wb')) && ($fp_in = gzopen($source, 'rb'))) {
         while (!feof($fp_in)) {
           fwrite($fp_out, gzread($fp_in, 1024 * 512));
@@ -139,7 +147,7 @@ class backup_migrate_filter_compression extends backup_migrate_filter {
   /**
    * Bzip encode a file.
    */
-  function _backup_migrate_bzip_encode($source, $dest) {
+  public function _backup_migrate_bzip_encode($source, $dest) {
     $success = FALSE;
     if (@function_exists("bzopen")) {
       if (($fp_out = bzopen($dest, 'w')) && ($fp_in = fopen($source, 'rb'))) {
@@ -160,7 +168,7 @@ class backup_migrate_filter_compression extends backup_migrate_filter {
   /**
    * Bzip decode a file.
    */
-  function _backup_migrate_bzip_decode($source, $dest) {
+  public function _backup_migrate_bzip_decode($source, $dest) {
     $success = FALSE;
     if (@function_exists("bzopen")) {
       if (($fp_out = fopen($dest, 'w')) && ($fp_in = bzopen($source, 'r'))) {
@@ -181,10 +189,10 @@ class backup_migrate_filter_compression extends backup_migrate_filter {
   /**
    * Zip encode a file.
    */
-  function _backup_migrate_zip_encode($source, $dest, $filename) {
+  public function _backup_migrate_zip_encode($source, $dest, $filename) {
     $success = FALSE;
     if (class_exists('ZipArchive')) {
-      $zip = new ZipArchive;
+      $zip = new ZipArchive();
       $res = $zip->open($dest, constant("ZipArchive::CREATE"));
       if ($res === TRUE) {
         $zip->addFile($source, $filename);
@@ -197,10 +205,10 @@ class backup_migrate_filter_compression extends backup_migrate_filter {
   /**
    * Zip decode a file.
    */
-  function _backup_migrate_zip_decode($source, $dest) {
+  public function _backup_migrate_zip_decode($source, $dest) {
     $success = FALSE;
     if (class_exists('ZipArchive')) {
-      $zip = new ZipArchive;
+      $zip = new ZipArchive();
       if (($fp_out = fopen($dest, "w")) && ($zip->open($source))) {
         $filename = ($zip->getNameIndex(0));
         if ($fp_in = $zip->getStream($filename)) {
@@ -220,11 +228,11 @@ class backup_migrate_filter_compression extends backup_migrate_filter {
    * Compress a file with the given settings.
    *  Also updates settings to reflect new file mime and file extension.
    */
-  function _backup_migrate_file_compress($file, $settings) {
+  public function _backup_migrate_file_compress($file, $settings) {
     switch ($settings->filters['compression']) {
       case "gzip":
         $from = $file->push_type('gzip');
-        if (!$success = $this->_backup_migrate_gzip_encode($from, $file->filepath(), 9)) {
+        if (!$success = $this->_backup_migrate_gzip_encode($from, $file->filepath(), 9, $settings)) {
           $file = NULL;
         }
         break;
@@ -247,7 +255,6 @@ class backup_migrate_filter_compression extends backup_migrate_filter {
     if (!$file) {
       _backup_migrate_message("Could not compress backup file. Try backing up without compression.", array(), 'error');
     }
-
     return $file;
   }
 
@@ -255,13 +262,13 @@ class backup_migrate_filter_compression extends backup_migrate_filter {
    * Decompress a file with the given settings.
    *  Also updates settings to reflect new file mime and file extension.
    */
-  function _backup_migrate_file_decompress($file) {
+  public function _backup_migrate_file_decompress($file, $settings) {
     $success = FALSE;
 
     switch ($file->type_id()) {
       case "gzip":
         $from = $file->pop_type();
-        $success = $this->_backup_migrate_gzip_decode($from->filepath(), $file->filepath());
+        $success = $this->_backup_migrate_gzip_decode($from->filepath(), $file->filepath(), $settings);
         break;
 
       case "bzip":
@@ -285,5 +292,5 @@ class backup_migrate_filter_compression extends backup_migrate_filter {
     }
     return $success ? $file : NULL;
   }
-}
 
+}

+ 136 - 38
sites/all/modules/contrib/admin/backup_migrate/includes/filters.encryption.inc

@@ -1,6 +1,5 @@
 <?php
 
-
 /**
  * @file
  * A filter for encrypting bckups with AES.
@@ -12,59 +11,77 @@
  * @ingroup backup_migrate_filters
  */
 class backup_migrate_filter_encryption extends backup_migrate_filter {
-  var $op_weights = array('backup' => 170, 'restore' => -170);
+  public $op_weights = array('backup' => 170, 'restore' => -170);
 
   /**
-   * This function is called on a backup file after the backup has been completed.
+   * Called on a backup file after the backup has been completed.
    */
-  function backup($file, &$settings) {
+  public function backup($file, $settings) {
     return $this->file_encrypt($file, $settings);
   }
 
   /**
-   * This function is called on a backup file before importing it.
+   * Called on a backup file before importing it.
    */
-  function restore($file, &$settings) {
+  public function restore($file, $settings) {
     return $this->file_decrypt($file);
   }
 
   /**
-   * Get the form for the settings for this filter.
+   * Gets the form for the settings for this filter.
    */
-  function backup_settings_default() {
+  public function backup_settings_default() {
     return array('encryption' => 'none');
   }
 
   /**
-   * Get the form for the settings for this filter.
+   * Gets the form for the settings for this filter.
    */
-  function backup_settings_form($settings) {
+  public function backup_settings_form($settings) {
     $form = array();
     $options = $this->_backup_migrate_get_encryption_form_item_options();
-    if (count($options) > 1) {
+
+    if (array_intersect_key($options, array_flip(array('aes', 'encrypt')))) {
       $form['file']['encryption'] = array(
         "#type" => "select",
-        "#title" => t("File Encryption (EXPERIMENTAL)"),
+        "#title" => t("File Encryption"),
         "#options" => $options,
         "#default_value" => @$settings['encryption'],
-        '#description' => t('Encrypted files can only be restored by Backup and Migrate and only on sites with the same encryption key. <strong>This functionality is experimental, and should only be used for testing.</strong>'),
+        '#description' => t('Encrypted files can only be restored by Backup and Migrate and only on sites with the same encryption key.'),
       );
     }
     else {
       $form['file']['encryption'] = array(
         "#type" => 'item',
         "#title" => t("File Encryption"),
-        "#markup" => t('Install the !link to enable backup file encryption.', array('!link' => l(t('AES Encryption Module'), 'http://drupal.org/project/aes'))),
+        "#markup" => t('Install the !link to enable backup file encryption.', array('!link' => l(t('Encrypt module'), 'http://drupal.org/project/encrypt'))),
+      );
+    }
+
+    // If the Encrypt method is available add a configuration field.
+    if (array_key_exists('encrypt', $options)) {
+      $form['file']['encrypt_config'] = array(
+        '#type' => 'select',
+        '#title' => t('Encryption Configuration'),
+        '#options' => encrypt_get_configs_as_options(),
+        '#default_value' => @$settings['encrypt_config'],
+        '#description' => t('Select a configuration to use for encryption.'),
+        '#states' => array(
+          'visible' => array(
+            ':input[name="filters[encryption]"]' => array('value' => 'encrypt'),
+          ),
+        ),
       );
+
     }
 
     return $form;
   }
 
   /**
-   * Return a list of backup filetypes.
+   * Returns a list of backup filetypes.
    */
-  function file_types() {
+  public function file_types() {
     return array(
       "aes" => array(
         "extension" => "aes",
@@ -72,29 +89,39 @@ class backup_migrate_filter_encryption extends backup_migrate_filter {
         "backup" => TRUE,
         "restore" => TRUE,
       ),
+      "encrypt" => array(
+        "extension" => "encrypt",
+        "filemime" => "application/octet-stream",
+        "backup" => TRUE,
+        "restore" => TRUE,
+      ),
     );
   }
 
   /**
-   * Get the compression options as an options array for a form item.
+   * Gets the compression options as an options array for a form item.
    */
-  function _backup_migrate_get_encryption_form_item_options() {
+  public function _backup_migrate_get_encryption_form_item_options() {
     $options = array();
     $options = array('' => t('No Encryption'));
     if (@function_exists("aes_encrypt")) {
       $options['aes'] = t("AES Encryption");
     }
+    if (@function_exists("encrypt")) {
+      $options['encrypt'] = t("Encryption With Encrypt Module");
+    }
     return $options;
   }
 
   /**
-   * AES encrypt a file.
+   * AES encrypts a file.
    */
-  function aes_encrypt($source, $dest) {
+  public function aes_encrypt($source, $dest) {
     $success = FALSE;
     if (function_exists('aes_encrypt')) {
       if ($data = $source->get_contents()) {
-        // Add a marker to the end of the data so we can trim the padding on decrpypt.
+        // Add a marker to the end of the data so we can trim the padding on
+        // decrpypt.
         $data = pack("a*H2", $data, "80");
         if ($data = aes_encrypt($data, FALSE)) {
           $dest->put_contents($data);
@@ -106,9 +133,9 @@ class backup_migrate_filter_encryption extends backup_migrate_filter {
   }
 
   /**
-   * Gzip decode a file.
+   * AES decodes a file.
    */
-  function aes_decrypt($source, $dest) {
+  public function aes_decrypt($source, $dest) {
     $success = FALSE;
     if (function_exists('aes_decrypt')) {
       if ($data = $source->get_contents()) {
@@ -124,10 +151,54 @@ class backup_migrate_filter_encryption extends backup_migrate_filter {
   }
 
   /**
-   * Compress a file with the given settings.
-   *  Also updates settings to reflect new file mime and file extension.
+   * Encrypts a file with the Encrypt module.
    */
-  function file_encrypt($file, $settings) {
+  public function encrypt($source, $dest, $settings) {
+    $success = FALSE;
+    if (function_exists('encrypt')) {
+      if (!empty($settings->filters['encrypt_config'])) {
+        $config_name = $settings->filters['encrypt_config'];
+      }
+      else {
+        $config = encrypt_get_default_config();
+        $config_name = $config['name'];
+      }
+      if ($data = $source->get_contents()) {
+        // Add a marker to the end of the data so we can trim the padding on decrypt.
+        $data = pack("a*H2", $data, "80");
+        if ($data = encrypt($data, array('base64' => FALSE), NULL, NULL, $config_name)) {
+          $dest->put_contents($data);
+          $success = TRUE;
+        }
+      }
+    }
+    return $success;
+  }
+
+  /**
+   * Decrypts a file with the Encrypt module.
+   */
+  public function decrypt($source, $dest) {
+    $success = FALSE;
+    if (function_exists('decrypt')) {
+      if ($data = $source->get_contents()) {
+        if ($data = decrypt($data)) {
+          // Trim all the padding zeros plus our non-zero marker.
+          $data = substr(rtrim($data, "\0"), 0, -1);
+          $dest->put_contents($data);
+          $success = TRUE;
+        }
+      }
+    }
+    return $success;
+  }
+
+  /**
+   * Encrypts a file with the given settings.
+   *
+   * Also updates settings to reflect new file mime and file extension.
+   */
+  public function file_encrypt($file, $settings) {
     if (!empty($settings->filters['encryption'])) {
       switch ($settings->filters['encryption']) {
         case "aes":
@@ -137,6 +208,14 @@ class backup_migrate_filter_encryption extends backup_migrate_filter {
             $file = NULL;
           }
           break;
+
+        case "encrypt":
+          $from = $file->push_type('encrypt');
+          $from = new backup_file(array('filepath' => $from));
+          if (!$success = $this->encrypt($from, $file, $settings)) {
+            $file = NULL;
+          }
+          break;
       }
       if (!$file) {
         _backup_migrate_message("Could not encrypt backup file. Try backing up without encryption.", array(), 'error');
@@ -146,10 +225,11 @@ class backup_migrate_filter_encryption extends backup_migrate_filter {
   }
 
   /**
-   * Decompress a file with the given settings.
-   *  Also updates settings to reflect new file mime and file extension.
+   * Decrypts a file with the given settings.
+   *
+   * Also updates settings to reflect new file mime and file extension.
    */
-  function file_decrypt($file) {
+  public function file_decrypt($file) {
     $success = FALSE;
     if ($file) {
       switch ($file->type_id()) {
@@ -157,21 +237,39 @@ class backup_migrate_filter_encryption extends backup_migrate_filter {
           $from = $file->pop_type();
           $success = $this->aes_decrypt($from, $file);
           break;
+
+        case "encrypt":
+          $from = $file->pop_type();
+          $success = $this->decrypt($from, $file);
+          break;
+
         default:
           return $file;
-        break;
-    }
-  
+      }
+
       if (!$success) {
-        if (function_exists('aes_decrypt')) {
-          _backup_migrate_message("Could not decrpyt backup file. Please check that the file is valid and that the encryption key of the server matches the server that created the backup.", array(), 'error');
-        }
-        else {
-          _backup_migrate_message('You must install the !link to restore encrypted backkups.', array('!link' => l(t('AES Encryption Module'), 'http://drupal.org/project/aes')), 'error');
+        switch ($file->type_id()) {
+          case 'aes':
+            if (function_exists('aes_decrypt')) {
+              _backup_migrate_message("Could not decrpyt backup file. Please check that the file is valid and that the encryption key of the server matches the server that created the backup.", array(), 'error');
+            }
+            else {
+              _backup_migrate_message('You must install the !link to restore encrypted backkups.', array('!link' => l(t('AES Encryption Module'), 'http://drupal.org/project/aes')), 'error');
+            }
+            break;
+
+          case 'encrypt':
+            if (function_exists('decrypt')) {
+              _backup_migrate_message("Could not decrypt backup file. Please check that the file is valid and that the encryption key of the server matches the server that created the backup.", array(), 'error');
+            }
+            else {
+              _backup_migrate_message('You must install the !link to restore encrypted backups.', array('!link' => l(t('Encrypt module'), 'http://drupal.org/project/encrypt')), 'error');
+            }
+            break;
         }
       }
     }
     return $success ? $file : NULL;
   }
-}
 
+}

+ 88 - 48
sites/all/modules/contrib/admin/backup_migrate/includes/filters.inc

@@ -1,25 +1,24 @@
 <?php
 
-
 /**
  * @file
  * All of the filter handling code needed for Backup and Migrate.
  */
 
 /**
- * Get the available destination types.
+ * Gets the available destination types.
  */
 function backup_migrate_get_filters($op = NULL) {
-  static $filters = NULL;
+  $filters = &drupal_static('backup_migrate_get_filters', NULL);
   if ($filters === NULL) {
     $filters = array();
     $definitions = module_invoke_all('backup_migrate_filters');
     foreach ($definitions as $definition) {
       // Include the necessary file if specified by the filter.
       if (!empty($definition['file'])) {
-        require_once './'. $definition['file'];
+        require_once './' . $definition['file'];
       }
-      $filters[] = new $definition['class'];
+      $filters[] = new $definition['class']();
     }
   }
   $sort = array();
@@ -32,37 +31,37 @@ function backup_migrate_get_filters($op = NULL) {
 }
 
 /**
- * Implementation of hook_backup_migrate_filters().
+ * Implements hook_backup_migrate_filters().
  *
  * Get the built in Backup and Migrate filters.
  */
 function backup_migrate_backup_migrate_filters() {
   return array(
     'backup_restore' => array(
-      'file' => drupal_get_path('module', 'backup_migrate') .'/includes/filters.backup_restore.inc',
+      'file' => drupal_get_path('module', 'backup_migrate') . '/includes/filters.backup_restore.inc',
       'class' => 'backup_migrate_filter_backup_restore',
     ),
     'compression' => array(
-      'file' => drupal_get_path('module', 'backup_migrate') .'/includes/filters.compression.inc',
+      'file' => drupal_get_path('module', 'backup_migrate') . '/includes/filters.compression.inc',
       'class' => 'backup_migrate_filter_compression',
     ),
     'encryption' => array(
-      'file' => drupal_get_path('module', 'backup_migrate') .'/includes/filters.encryption.inc',
+      'file' => drupal_get_path('module', 'backup_migrate') . '/includes/filters.encryption.inc',
       'class' => 'backup_migrate_filter_encryption',
     ),
     'statusnotify' => array(
-      'file' => drupal_get_path('module', 'backup_migrate') .'/includes/filters.statusnotify.inc',
+      'file' => drupal_get_path('module', 'backup_migrate') . '/includes/filters.statusnotify.inc',
       'class' => 'backup_migrate_filter_statusnotify',
     ),
     'utils' => array(
-      'file' => drupal_get_path('module', 'backup_migrate') .'/includes/filters.utils.inc',
+      'file' => drupal_get_path('module', 'backup_migrate') . '/includes/filters.utils.inc',
       'class' => 'backup_migrate_filter_utils',
     ),
   );
 }
 
 /**
- * Invoke the given method on all of the available filters.
+ * Invokes the given method on all of the available filters.
  */
 function backup_migrate_filters_invoke_all() {
   $args    = func_get_args();
@@ -96,42 +95,47 @@ function backup_migrate_filters_invoke_all() {
 }
 
 /**
- * Filter a backup file before sending it to the destination.
+ * Filters a backup file before sending it to the destination.
  */
 function backup_migrate_filters_backup($file, &$settings) {
+  backup_migrate_filters_invoke_all('pre_backup', $file, $settings);
   $filters = backup_migrate_get_filters('backup');
   foreach ($filters as $filter) {
     if ($file) {
       $file = $filter->backup($file, $settings);
     }
   }
+  backup_migrate_filters_invoke_all('post_backup', $file, $settings);
+
   return $file;
 }
 
 /**
- * Filter a backup file before sending it to the destination.
+ * Filters a backup file before sending it to the destination.
  */
 function backup_migrate_filters_restore($file, &$settings) {
+  backup_migrate_filters_invoke_all('pre_restore', $file, $settings);
   $filters = backup_migrate_get_filters('restore');
   foreach ($filters as $filter) {
     if ($file) {
       $file = $filter->restore($file, $settings);
     }
   }
+  backup_migrate_filters_invoke_all('post_restore', $file, $settings);
   return $file;
 }
 
 /**
- * Get the backup settings for all of the filters.
+ * Gets the backup settings for all of the filters.
  */
 function backup_migrate_filters_settings_form($settings, $op) {
-  $out = backup_migrate_filters_invoke_all($op .'_settings_form', $settings);
+  $out = backup_migrate_filters_invoke_all($op . '_settings_form', $settings);
   $out = backup_migrate_filters_settings_form_set_parents($out);
   return $out;
 }
 
 /**
- * Add a form parent to the filter settings so that the filter values are saved in the right table.
+ * Adds form parent to filter settings so the values are saved in correct table.
  */
 function backup_migrate_filters_settings_form_set_parents($form) {
   foreach (element_children($form) as $key) {
@@ -143,27 +147,51 @@ function backup_migrate_filters_settings_form_set_parents($form) {
   return $form;
 }
 
-
 /**
- * Validate all the filters.
+ * Validates all the filters.
  */
 function backup_migrate_filters_settings_form_validate($op, $form, &$form_state) {
-  //backup_migrate_filters_invoke_all($op .'_settings_form_validate', $form, $form_state);
+  backup_migrate_filters_invoke_all($op . '_settings_form_validate', $form, $form_state);
 }
 
 /**
- * Submit all of the filters.
+ * Submits all of the filters.
  */
 function backup_migrate_filters_settings_form_submit($op, $form, &$form_state) {
-  //backup_migrate_filters_invoke_all($op .'_settings_form_submit', $form, $form_state);
+  backup_migrate_filters_invoke_all($op . '_settings_form_submit', $form, $form_state);
 }
 
-
 /**
- * Get the default settings for the filters.
+ * Gets the default settings for the filters.
  */
 function backup_migrate_filters_settings_default($op) {
-  return backup_migrate_filters_invoke_all($op .'_settings_default');
+  return backup_migrate_filters_invoke_all($op . '_settings_default');
+}
+
+/**
+ * Get the backup settings for all of the filters.
+ */
+function backup_migrate_filters_before_action_form($settings, $op) {
+  $out = array();
+  $out += backup_migrate_filters_invoke_all('before_action_form', $op, $settings);
+  $out += backup_migrate_filters_invoke_all('before_' . $op . '_form', $settings);
+  return $out;
+}
+
+/**
+ * Get the backup settings for all of the filters.
+ */
+function backup_migrate_filters_before_action_form_validate($settings, $op, $form, &$form_state) {
+  backup_migrate_filters_invoke_all('before_action_form_validate', $op, $settings, $form, $form_state);
+  backup_migrate_filters_invoke_all('before_' . $op . '_form_validate', $settings, $form, $form_state);
+}
+
+/**
+ * Get the backup settings for all of the filters.
+ */
+function backup_migrate_filters_before_action_form_submit($settings, $op, $form, &$form_state) {
+  backup_migrate_filters_invoke_all('before_action_form_submit', $op, $settings, $form, $form_state);
+  backup_migrate_filters_invoke_all('before_' . $op . '_form_submit', $settings, $form, $form_state);
 }
 
 /**
@@ -177,13 +205,13 @@ function backup_migrate_filters_file_types() {
  * A base class for basing filters on.
  */
 class backup_migrate_filter {
-  var $weight = 0;
-  var $op_weights = array();
+  public $weight = 0;
+  public $op_weights = array();
 
   /**
    * Get the weight of the filter for the given op.
    */
-  function weight($op = NULL) {
+  public function weight($op = NULL) {
     if ($op && isset($this->op_weights[$op])) {
       return $this->op_weights[$op];
     }
@@ -193,98 +221,110 @@ class backup_migrate_filter {
   /**
    * Get the form for the settings for this filter.
    */
-  function backup_settings_default() {
+  public function backup_settings_default() {
     return array();
   }
 
   /**
    * Get the form for the settings for this filter.
    */
-  function backup_settings_form($settings) {
+  public function backup_settings_form($settings) {
     return array();
   }
 
   /**
    * Get the form for the settings for this filter.
    */
-  function backup_settings_form_validate($form, &$form_state) {
+  public function backup_settings_form_validate($form, &$form_state) {
   }
 
   /**
    * Submit the settings form. Any values returned will be saved.
    */
-  function backup_settings_form_submit($form, &$form_state) {
-    return $form_state['values'];
+  public function backup_settings_form_submit($form, &$form_state) {
   }
 
   /**
    * Get the form for the settings for this filter.
    */
-  function restore_settings_default() {
+  public function restore_settings_default() {
     return array();
   }
 
   /**
    * Get the form for the settings for this filter.
    */
-  function restore_settings_form($settings) {
+  public function restore_settings_form($settings) {
     return array();
   }
 
   /**
    * Get the form for the settings for this filter.
    */
-  function restore_settings_form_validate($form, &$form_state) {
+  public function restore_settings_form_validate($form, &$form_state) {
   }
 
   /**
    * Submit the settings form. Any values returned will be saved.
    */
-  function restore_settings_form_submit($form, &$form_state) {
+  public function restore_settings_form_submit($form, &$form_state) {
     return $form_state['values'];
   }
 
   /**
    * Get a list of file types handled by this filter.
    */
-  function file_types() {
+  public function file_types() {
     return array();
   }
 
   /**
    * Declare any default destinations for this filter.
    */
-  function destinations() {
+  public function destinations() {
     return array();
   }
 
-
   /**
-   * This function is called on a backup file after the backup has been completed.
+   * Called on a backup file after the backup has been completed.
    */
-  function backup($file, &$settings) {
+  public function backup($file, $settings) {
     return $file;
   }
 
+  /**
+   * This function is called immediately prior to backup.
+   */
+  public function pre_backup($file, $settings) {
+
+  }
+
+  /**
+   * This function is called immediately post backup.
+   */
+  public function post_backup($file, $settings) {
+
+  }
+
   /**
    * This function is called on a backup file before importing it.
    */
-  function restore($file, &$settings) {
+  public function restore($file, $settings) {
     return $file;
   }
 
   /**
-   * This function is called immediately prior to backup.
+   * This function is called immediately prior to restore.
    */
-  function pre_backup($source, $file, $settings) {
+  public function pre_restore($file, $settings) {
 
   }
 
   /**
-   * This function is called immediately post backup.
+   * This function is called immediately post restore.
    */
-  function post_backup($source, $file, $settings) {
+  public function post_restore($file, $settings) {
 
   }
-}
 
+}

+ 23 - 14
sites/all/modules/contrib/admin/backup_migrate/includes/filters.statusnotify.inc

@@ -1,6 +1,5 @@
 <?php
 
-
 /**
  * @file
  * A filter for compressing bckups with zip, gz bzip etc.
@@ -12,11 +11,11 @@
  * @ingroup backup_migrate_filters
  */
 class backup_migrate_filter_statusnotify extends backup_migrate_filter {
-  
+
   /**
    * Get the default backup settings for this filter.
    */
-  function backup_settings_default() {
+  public function backup_settings_default() {
     return array(
       'notify_success_enable' => FALSE,
       'notify_failure_enable' => FALSE,
@@ -28,14 +27,20 @@ class backup_migrate_filter_statusnotify extends backup_migrate_filter {
   /**
    * Get the form for the settings for this filter.
    */
-  function backup_settings_form($settings) {
+  public function backup_settings_form($settings) {
     $form = array();
     $form['advanced']['notify_success_enable'] = array(
       "#type" => 'checkbox',
       "#title" => t("Send an email if backup succeeds"),
       "#default_value" => @$settings['notify_success_enable'],
     );
-    $form['advanced']['notify_success_email'] = array(
+    $form['advanced']['notify_success_email_wrapper'] = array(
+      '#type' => 'backup_migrate_dependent',
+      '#dependencies' => array(
+        'filters[notify_success_enable]' => TRUE,
+      ),
+    );
+    $form['advanced']['notify_success_email_wrapper']['notify_success_email'] = array(
       "#type" => "textfield",
       "#title" => t("Email Address for Success Notices"),
       "#default_value" => @$settings['notify_success_email'],
@@ -45,7 +50,13 @@ class backup_migrate_filter_statusnotify extends backup_migrate_filter {
       "#title" => t("Send an email if backup fails"),
       "#default_value" => @$settings['notify_failure_enable'],
     );
-    $form['advanced']['notify_failure_email'] = array(
+    $form['advanced']['notify_failure_email_wrapper'] = array(
+      '#type' => 'backup_migrate_dependent',
+      '#dependencies' => array(
+        'filters[notify_failure_enable]' => TRUE,
+      ),
+    );
+    $form['advanced']['notify_failure_email_wrapper']['notify_failure_email'] = array(
       "#type" => "textfield",
       "#title" => t("Email Address for Failure Notices"),
       "#default_value" => @$settings['notify_failure_email'],
@@ -56,41 +67,39 @@ class backup_migrate_filter_statusnotify extends backup_migrate_filter {
   /**
    * Send the success email.
    */
-  function backup_succeed($settings) {
+  public function backup_succeed($settings) {
     if (@$settings->filters['notify_success_enable'] && $to = @$settings->filters['notify_success_email']) {
       $messages = $this->get_messages();
-      $subject = t('!site backup succeeded', array('!site' => variable_get('site_name', 'Drupal site')));
       if ($messages = $this->get_messages()) {
         $body = t("The site backup has completed successfully with the following messages:\n!messages", array('!messages' => $messages));
       }
       else {
         $body = t("The site backup has completed successfully.\n");
       }
-      mail($settings->filters['notify_success_email'], $subject, $body);
+      drupal_mail('backup_migrate', 'backup_succeed', $settings->filters['notify_success_email'], language_default(), array('body' => $body));
     }
   }
 
   /**
    * Send the failure email.
    */
-  function backup_fail($settings) {
+  public function backup_fail($settings) {
     if (@$settings->filters['notify_failure_enable'] && $to = @$settings->filters['notify_failure_email']) {
       $messages = $this->get_messages();
-      $subject = t('!site backup failed', array('!site' => variable_get('site_name', 'Drupal site')));
       if ($messages = $this->get_messages()) {
         $body = t("The site backup has failed with the following messages:\n!messages", array('!messages' => $messages));
       }
       else {
         $body = t("The site backup has failed for an unknown reason.");
       }
-      mail($settings->filters['notify_failure_email'], $subject, $body);
+      drupal_mail('backup_migrate', 'backup_fail', $settings->filters['notify_failure_email'], language_default(), array('body' => $body));
     }
   }
 
   /**
    * Render the messages and errors for the email.
    */
-  function get_messages() {
+  public function get_messages() {
     $out = "";
     $messages = _backup_migrate_messages();
     foreach ($messages as $message) {
@@ -98,5 +107,5 @@ class backup_migrate_filter_statusnotify extends backup_migrate_filter {
     }
     return $out;
   }
-}
 
+}

+ 119 - 51
sites/all/modules/contrib/admin/backup_migrate/includes/filters.utils.inc

@@ -1,23 +1,23 @@
 <?php
 
-
 /**
  * @file
  * A filter to run some basic utility functions. Basically any useful option not big enough to justify it's own class.
  */
 
 /**
- * A filter to send a notification email on success or failure of backup.
+ * A filter to run some basic utility functions.
  *
  * @ingroup backup_migrate_filters
  */
 class backup_migrate_filter_utils extends backup_migrate_filter {
-  var $saved_devel_query = NULL;
+  public $op_weights = array('pre_backup' => -1000, 'post_backup' => 1000);
+  public $saved_devel_query = NULL;
 
   /**
    * Get the default backup settings for this filter.
    */
-  function backup_settings_default() {
+  public function backup_settings_default() {
     return array(
       'utils_disable_query_log' => TRUE,
       'utils_site_offline' => FALSE,
@@ -28,7 +28,7 @@ class backup_migrate_filter_utils extends backup_migrate_filter {
   /**
    * Get the default restore settings for this filter.
    */
-  function restore_settings_default() {
+  public function restore_settings_default() {
     return array(
       'utils_disable_query_log' => TRUE,
       'utils_site_offline' => FALSE,
@@ -38,7 +38,7 @@ class backup_migrate_filter_utils extends backup_migrate_filter {
   /**
    * Get the form for the backup settings for this filter.
    */
-  function backup_settings_form($settings) {
+  public function backup_settings_form($settings) {
     $form = array();
     if (module_exists('devel') && variable_get('dev_query', 0)) {
       $form['database']['utils_disable_query_log'] = array(
@@ -54,17 +54,35 @@ class backup_migrate_filter_utils extends backup_migrate_filter {
       '#default_value' => !empty($settings['utils_site_offline']) ? $settings['utils_site_offline'] : NULL,
       '#description' => t('Take the site offline during backup and show a maintenance message. Site will be taken back online once the backup is complete.'),
     );
-    $form['advanced']['utils_site_offline_message'] = array(
+    $form['advanced']['utils_site_offline_message_wrapper'] = array(
+      '#type' => 'backup_migrate_dependent',
+      '#dependencies' => array(
+        'filters[utils_site_offline]' => TRUE,
+      ),
+    );
+    $form['advanced']['utils_site_offline_message_wrapper']['utils_site_offline_message'] = array(
       '#type' => 'textarea',
       '#title' => t('Site off-line message'),
-      '#default_value' => !empty($settings['utils_site_offline_message']) ? $settings['utils_site_offline_message'] : variable_get('maintenance_mode_message', t('@site is currently under maintenance. We should be back shortly. Thank you for your patience.', array('@site' => variable_get('site_name', 'Drupal')))),
-      '#description' => t('Message to show visitors when the site is in off-line mode.')
+      '#default_value' => !empty($settings['utils_site_offline_message']) ? $settings['utils_site_offline_message'] : variable_get('site_offline_message', t('@site is currently under maintenance. We should be back shortly. Thank you for your patience.', array('@site' => variable_get('site_name', 'Drupal')))),
+      '#description' => t('Message to show visitors when the site is in off-line mode.'),
     );
     $form['advanced']['utils_description'] = array(
       '#type' => 'textarea',
-      '#title' => t('Description'),
+      '#title' => t('Add a note'),
       '#default_value' => !empty($settings['utils_description']) ? $settings['utils_description'] : NULL,
-      '#description' => t('Add a short description to the backup file.'),
+      '#description' => t('Add a short note to the backup file.'),
+    );
+    $form['advanced']['use_cli'] = array(
+      "#type" => "checkbox",
+      "#title" => t("Use cli commands"),
+      "#default_value" => !empty($settings['use_cli']),
+      "#description" => t("Use the command line tools (mysqldump, tar, gzip etc.) if available. This can be faster for large sites but will not work on all servers. EXPERIMENTAL"),
+    );
+    $form['advanced']['ignore_errors'] = array(
+      "#type" => "checkbox",
+      "#title" => t("Ignore errors"),
+      "#default_value" => !empty($settings['ignore_errors']),
+      "#description" => t("Will attempt to complete backup even if certain recoverable errors occur. This may make the backup files invalid. Enable this if you have unreadable files that you want to ignore during backup."),
     );
 
     return $form;
@@ -73,7 +91,7 @@ class backup_migrate_filter_utils extends backup_migrate_filter {
   /**
    * Get the form for the restore settings for this filter.
    */
-  function restore_settings_form($settings) {
+  public function restore_settings_form($settings) {
     $form = array();
     if (module_exists('devel') && variable_get('dev_query', 0)) {
       $form['advanced']['utils_disable_query_log'] = array(
@@ -89,32 +107,70 @@ class backup_migrate_filter_utils extends backup_migrate_filter {
       '#default_value' => !empty($settings['utils_site_offline']) ? $settings['utils_site_offline'] : NULL,
       '#description' => t('Take the site offline during restore and show a maintenance message. Site will be taken back online once the restore is complete.'),
     );
-    $form['advanced']['utils_site_offline_message'] = array(
+    $form['advanced']['utils_site_offline_message_wrapper'] = array(
+      '#type' => 'backup_migrate_dependent',
+      '#dependencies' => array(
+        'filters[utils_site_offline]' => TRUE,
+      ),
+    );
+    $form['advanced']['utils_site_offline_message_wrapper']['utils_site_offline_message'] = array(
       '#type' => 'textarea',
       '#title' => t('Site off-line message'),
-      '#default_value' => !empty($settings['utils_site_offline_message']) ? $settings['utils_site_offline_message'] : variable_get('maintenance_mode_message', t('@site is currently under maintenance. We should be back shortly. Thank you for your patience.', array('@site' => variable_get('site_name', 'Drupal')))),
-      '#description' => t('Message to show visitors when the site is in off-line mode.')
+      '#default_value' => !empty($settings['utils_site_offline_message']) ? $settings['utils_site_offline_message'] : variable_get('site_offline_message', t('@site is currently under maintenance. We should be back shortly. Thank you for your patience.', array('@site' => variable_get('site_name', 'Drupal')))),
+      '#description' => t('Message to show visitors when the site is in off-line mode.'),
     );
+    $form['advanced']['utils_drop_all_tables'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Drop all tables before import (MySQL only)'),
+      '#default_value' => !empty($settings['utils_drop_all_tables']) ? $settings['utils_drop_all_tables'] : NULL,
+      '#description' => t('Drop all existing database tables before restoring the backup. This option is currently available on MySQL servers only.'),
+    );
+    $form['advanced']['use_cli'] = array(
+      "#type" => "checkbox",
+      "#title" => t("Use cli commands"),
+      "#default_value" => !empty($settings['use_cli']),
+      "#description" => t("Use the command line tools (mysqldump, tar, gzip etc.) if available. This can be faster for large sites but will not work on all servers. EXPERIMENTAL"),
+    );
+    // $form['advanced']['ignore_errors'] = array(
+    //   "#type" => "checkbox",
+    //   "#title" => t("Ignore errors"),
+    //   "#default_value" => !empty($settings['ignore_errors']),
+    //   "#description" => t("Will attempt to complete restore even if certain recoverable errors occur. This may could corrupt your site."),
+    // );
     return $form;
   }
 
-  function pre_backup($source, $file, $settings) {
+  /**
+   *
+   */
+  public function pre_backup($file, $settings) {
     $this->take_site_offline($settings);
     $this->disable_devel_query($settings);
   }
 
-  function post_backup($source, $file, $settings) {
+  /**
+   *
+   */
+  public function post_backup($file, $settings) {
     $this->enable_devel_query($settings);
     $this->take_site_online($settings);
-    $this->add_file_info($file, $settings);
+    if ($file) {
+      $this->add_file_info($file, $settings);
+    }
   }
 
-  function pre_restore($file, $settings) {
+  /**
+   *
+   */
+  public function pre_restore($file, $settings) {
     $this->disable_devel_query($settings);
     $this->take_site_offline($settings);
   }
 
-  function post_restore($file, $settings) {
+  /**
+   *
+   */
+  public function post_restore($file, $settings) {
     $this->enable_devel_query($settings);
     $this->take_site_online($settings);
   }
@@ -122,7 +178,7 @@ class backup_migrate_filter_utils extends backup_migrate_filter {
   /**
    * Disable devel query logging if it's active and the user has chosen to do so.
    */
-  function disable_devel_query($settings) {
+  public function disable_devel_query($settings) {
     $this->saved_devel_query = variable_get('dev_query', 0);
     if (module_exists('devel') && variable_get('dev_query', 0) && !empty($settings->filters['utils_disable_query_log'])) {
       variable_set('dev_query', 0);
@@ -132,7 +188,7 @@ class backup_migrate_filter_utils extends backup_migrate_filter {
   /**
    * Restore devel query to previous state.
    */
-  function enable_devel_query($settings) {
+  public function enable_devel_query($settings) {
     if (module_exists('devel')) {
       variable_set('dev_query', $this->saved_devel_query);
     }
@@ -141,49 +197,61 @@ class backup_migrate_filter_utils extends backup_migrate_filter {
   /**
    * Add the backup metadata to the file.
    */
-  function add_file_info($file, $settings) {
-    $file->file_info['description']       = $settings->filters['utils_description'];
-    $file->file_info['datestamp']         = time();
-    $file->file_info['generator']         = 'Backup and Migrate (http://drupal.org/project/backup_migrate)';
-    $file->file_info['generatorversion']  = BACKUP_MIGRATE_VERSION;
-    $file->file_info['sites'] = array(
-        '0' => array(
-          'version' => VERSION,
-          'name' => variable_get('site_name', ''),
-          'url' => url('', array('absolute' => TRUE)),
-        ),
-      );
-    
+  public function add_file_info($file, $settings) {
+    $file->file_info['description']      = $settings->filters['utils_description'];
+    $file->file_info['datestamp']        = time();
+    $file->file_info['generator']        = 'Backup and Migrate (http://drupal.org/project/backup_migrate)';
+    $file->file_info['generatorversion'] = BACKUP_MIGRATE_VERSION;
+    $file->file_info['siteurl']          = url('', array('absolute' => TRUE));
+    $file->file_info['sitename']         = variable_get('site_name', '');
+    $file->file_info['drupalversion']    = VERSION;
+    $file->calculate_filesize();
+
+    $source                            = $settings->get('source');
+    $file->file_info['bam_sourceid']   = $source->get('id');
+    $file->file_info['bam_sourcetype'] = $source->get('subtype');
+    $file->file_info['bam_sourcename'] = $source->get('name');
+
+    // Add any additional info that has been added to the settings by other plugins.
+    if (!empty($settings->file_info)) {
+      $file->file_info += $settings->file_info;
+    }
   }
 
   /**
    * Take the site offline if configured to do so.
    */
-  function take_site_offline($settings) {
-    // Save the current state of the site in case a restore overwrites it.
-    $this->saved_site_offline = variable_get('maintenance_mode', 0);
-    if (@$settings->filters['utils_site_offline']) {
-      $this->saved_site_offline_message = variable_get('maintenance_mode_message', NULL);
-      if (!empty($settings->filters['utils_site_offline_message'])) {
+  public function take_site_offline($settings) {
+    // If the site is already offline then don't do anything.
+    if (!variable_get('maintenance_mode', 0)) {
+      // Save the current state of the site in case a restore overwrites it.
+      if (!empty($settings->filters['utils_site_offline'])) {
+        $this->saved_site_offline = TRUE;
         $this->saved_site_offline_message = variable_get('maintenance_mode_message', NULL);
-        variable_set('maintenance_mode_message', $settings->filters['utils_site_offline_message']);
+        if (!empty($settings->filters['utils_site_offline_message'])) {
+          $this->saved_site_offline_message = variable_get('maintenance_mode_message', NULL);
+          variable_set('maintenance_mode_message', $settings->filters['utils_site_offline_message']);
+        }
+        variable_set('maintenance_mode', 1);
+        _backup_migrate_message('Site was taken offline.');
       }
-      variable_set('maintenance_mode', 1);
-      _backup_migrate_message('Site was taken offline.');
     }
   }
 
   /**
    * Take the site online again after backup or restore.
    */
-  function take_site_online($settings) {
+  public function take_site_online($settings) {
     // Take the site back off/online because the restored db may have changed that setting.
-    variable_set('maintenance_mode', $this->saved_site_offline);
-    if ($settings->filters['utils_site_offline']) {
-      if (!empty($this->saved_site_offline_message)) {
-        variable_set('maintenance_mode_message', $this->saved_site_offline_message);
+    if (variable_get('maintenance_mode', 0) && !empty($this->saved_site_offline)) {
+      variable_set('maintenance_mode', 0);
+      if ($settings->filters['utils_site_offline']) {
+        if (!empty($this->saved_site_offline_message)) {
+          variable_set('maintenance_mode_message', $this->saved_site_offline_message);
+        }
+        _backup_migrate_message('Site was taken online.');
       }
-      _backup_migrate_message('Site was taken online.');
     }
   }
-}
+
+}

+ 529 - 0
sites/all/modules/contrib/admin/backup_migrate/includes/locations.inc

@@ -0,0 +1,529 @@
+<?php
+
+/**
+ * @file
+ * All of the location handling code needed for Backup and Migrate.
+ */
+
+require_once dirname(__FILE__) . '/crud.inc';
+
+/**
+ * Get the available location types.
+ */
+function backup_migrate_get_location_subtypes() {
+  require_once dirname(__FILE__) . '/crud.inc';
+  require_once dirname(__FILE__) . '/destinations.inc';
+  require_once dirname(__FILE__) . '/sources.inc';
+
+  return backup_migrate_crud_subtypes('destination') + backup_migrate_crud_subtypes('source');
+}
+
+/**
+ * Implements hook_backup_migrate_locations().
+ *
+ * Get the built in backup locations and those in the db.
+ */
+function backup_migrate_backup_migrate_locations() {
+  require_once dirname(__FILE__) . '/filters.inc';
+
+  $out = array();
+
+  // Allow the filter plugins to declare the default locations.
+  $out += backup_migrate_filters_invoke_all('locations');
+
+  return $out;
+}
+
+/**
+ * Get all the available backup location.
+ *
+ * @param $op
+ *   The operation which will be performed on the location. Hooks can use this
+ *   to return only those locations appropriate for the given op.
+ *   Options include:
+ *    'manual backup' - locations available for manual backup
+ *    'scheduled backup' - locations available for schedules backup
+ *    'list files' - locations whose backup files can be listed
+ *    'restore' - locations whose files can be restored from
+ *    'all' - all available locations should be returned
+ */
+function backup_migrate_get_locations($op = 'all') {
+  $locations = &drupal_static('backup_migrate_get_locations', NULL);
+
+  // Get the list of locations and cache them locally.
+  if ($locations === NULL) {
+    $locations = backup_migrate_crud_get_items('location');
+  }
+
+  // Return all if that's what was asked for.
+  if ($op == 'all') {
+    return $locations;
+  }
+
+  // Return only those locations which support the given op.
+  $out = array();
+  if ($locations) {
+    foreach ($locations as $key => $location) {
+      if ($location->op($op)) {
+        $out[$key] = $location;
+      }
+    }
+  }
+  return $out;
+}
+
+/**
+ * Get the location of the given id.
+ */
+function backup_migrate_get_location($id) {
+  $locations = backup_migrate_get_locations('all');
+  return empty($locations[$id]) ? NULL : $locations[$id];
+}
+
+/**
+ * A base class for creating locations.
+ */
+class backup_migrate_location extends backup_migrate_item {
+  public $db_table = "backup_migrate_destinations";
+  public $type_name = "location";
+  public $default_values = array('settings' => array());
+  public $singular = 'location';
+  public $plural = 'locations';
+  public $title_plural = 'Locations';
+  public $title_singular = 'Location';
+
+  public $subtype = "";
+  public $supported_ops = array();
+
+  /**
+   * This function is not supposed to be called. It is just here to help the po extractor out.
+   */
+  public function strings() {
+    // Help the pot extractor find these strings.
+    t('location');
+    t('locations');
+    t('Location');
+    t('Locations');
+  }
+
+  /**
+   *
+   */
+  public function ops() {
+    return $this->supported_ops;
+  }
+
+  /**
+   * Does this location support the given operation.
+   */
+  public function op($op) {
+    $ops = (array) $this->ops();
+    return in_array($op, $ops);
+  }
+
+  /**
+   * Remove the given op from the support list.
+   */
+  public function remove_op($op) {
+    $key = array_search($op, $this->supported_ops);
+    if ($key !== FALSE) {
+      unset($this->supported_ops[$key]);
+    }
+  }
+
+  /**
+   *
+   */
+  public function get_name() {
+    return @$this->name;
+  }
+
+  /**
+   *
+   */
+  public function set_name($name) {
+    return $this->name = $name;
+  }
+
+  /**
+   *
+   */
+  public function set_location($location) {
+    $this->location = $location;
+  }
+
+  /**
+   *
+   */
+  public function get_location() {
+    return @$this->location;
+  }
+
+  /**
+   *
+   */
+  public function get_display_location() {
+    return $this->get_location();
+  }
+
+  /**
+   *
+   */
+  public function settings($key = NULL) {
+    $out = $this->settings;
+    if ($key) {
+      $out = isset($out[$key]) ? $out[$key] : NULL;
+    }
+    return $out;
+  }
+
+  /**
+   * Get the type name of this location for display to the user.
+   */
+  public function get_subtype_name() {
+    if ($type = $this->get('subtype')) {
+      $types = $this->location_types();
+      return isset($types[$type]['type_name']) ? $types[$type]['type_name'] : $type;
+    }
+  }
+
+  /**
+   * Get the edit form for the item.
+   */
+  public function edit_form() {
+    if (!empty($this->supported_ops)) {
+      $form = parent::edit_form();
+      $form['subtype'] = array(
+        "#type" => "value",
+        "#default_value" => $this->get('subtype'),
+      );
+    }
+    else {
+      $types = $this->location_types();
+      $items = array();
+      // If no (valid) node type has been provided, display a node type overview.
+      foreach ($types as $key => $type) {
+        if (@$type['can_create']) {
+          $type_url_str = str_replace('_', '-', $key);
+          $out = '<dt>' . l($type['type_name'], BACKUP_MIGRATE_MENU_PATH . "/settings/$this->type_name/add/$type_url_str", array('attributes' => array('title' => t('Add a new @s location.', array('@s' => $type['type_name']))))) . '</dt>';
+          $out .= '<dd>' . filter_xss_admin($type['description']) . '</dd>';
+          $items[] = $out;
+        }
+      }
+      if (count($items)) {
+        $output = t('Choose the type of location you would like to create:') . '<dl>' . implode('', $items) . '</dl>';
+      }
+      else {
+        $output = t('No types available.');
+      }
+      $form['select_type'] = array(
+        '#type' => 'markup',
+        '#markup' => $output,
+      );
+    }
+    return $form;
+  }
+
+  /**
+   * Get the available location types.
+   */
+  public function location_types() {
+    return backup_migrate_get_location_subtypes();
+  }
+
+  /**
+   * Get the message to send to the user when confirming the deletion of the item.
+   */
+  public function delete_confirm_message() {
+    return t('Are you sure you want to delete the %name?', array('%name' => $this->get_name()));
+  }
+
+  /**
+   * Get the columns needed to list the type.
+   */
+  public function get_list_column_info() {
+    $out = parent::get_list_column_info();
+    $out = array(
+      'name'                  => array('title' => t('Name')),
+      'subtype_name'    => array('title' => t('Type')),
+      'display_location'      => array('title' => t('Location')),
+    ) + $out;
+    return $out;
+  }
+
+  /**
+   * Get a row of data to be used in a list of items of this type.
+   */
+  public function get_list_row() {
+    $out = parent::get_list_row();
+
+    // Suppress locations with no actions as there's no value in showing them (and they may confuse new users).
+    if (empty($out['actions'])) {
+      return NULL;
+    }
+    return $out;
+  }
+
+  /**
+   * Get the action links for a location.
+   */
+  public function get_action_links() {
+    $out = parent::get_action_links();
+    $item_id = $this->get_id();
+
+    // Don't display the download/delete/restore ops if they are not available for this location.
+    if ($this->op('list files') && user_access("access backup files")) {
+      $out = array('list files' => l(t("list files"), BACKUP_MIGRATE_MENU_PATH . "/$this->type_name/list/files/" . $item_id)) + $out;
+    }
+    if (!$this->op('configure') || !user_access('administer backup and migrate')) {
+      unset($out['edit']);
+    }
+    return $out;
+  }
+
+  /**
+   * Determine if we can read the given file.
+   */
+  public function can_read_file($file_id) {
+    return $this->op('restore');
+  }
+
+  /**
+   * Get the form for the settings for this location type.
+   */
+  public function settings_default() {
+    return array();
+  }
+
+  /**
+   * Get the form for the settings for this location.
+   */
+  public function settings_form($form) {
+    return $form;
+  }
+
+  /**
+   * Validate the form for the settings for this location.
+   */
+  public function settings_form_validate($form_values) {
+  }
+
+  /**
+   * Submit the settings form. Any values returned will be saved.
+   */
+  public function settings_form_submit($form_values) {
+    return $form_values;
+  }
+
+  /**
+   * Get the form for the settings for this filter.
+   */
+  public function backup_settings_default() {
+    return array();
+  }
+
+  /**
+   * Get the form for the settings for this filter.
+   */
+  public function backup_settings_form($settings) {
+    return array();
+  }
+
+  /**
+   * Get the form for the settings for this filter.
+   */
+  public function backup_settings_form_validate($form, &$form_state) {
+  }
+
+  /**
+   * Submit the settings form. Any values returned will be saved.
+   */
+  public function backup_settings_form_submit($form, &$form_state) {
+  }
+
+  /**
+   * Get the form for the settings for this filter.
+   */
+  public function restore_settings_default() {
+    return array();
+  }
+
+  /**
+   * Get the form for the settings for this filter.
+   */
+  public function restore_settings_form($settings) {
+    return array();
+  }
+
+  /**
+   * Get the form for the settings for this filter.
+   */
+  public function restore_settings_form_validate($form_values) {
+  }
+
+  /**
+   * Submit the settings form. Any values returned will be saved.
+   */
+  public function restore_settings_form_submit($form_values) {
+    return $form_values;
+  }
+
+  /**
+   * Create a new location of the correct type.
+   */
+  public function create($params = array()) {
+    $out = NULL;
+    $types = backup_migrate_get_location_subtypes();
+    // Get the type passed in in the params, or if none, check the url for a valid type name.
+    // This is to allow new location type to be specified in the path.
+    $location_type = !empty($params['subtype']) ? $params['subtype'] : NULL;
+
+    if ($location_type && ($type = @$types[$location_type])) {
+      // Include the necessary file if specified by the type.
+      if (!empty($type['file'])) {
+        require_once './' . $type['file'];
+      }
+      $out = new $type['class']($params + array('subtype' => $location_type));
+    }
+
+    if (empty($out)) {
+      $out = parent::create($params);
+    }
+    return $out;
+  }
+
+  /**
+   * Get a url from the parts.
+   */
+  public function url($hide_password = TRUE) {
+    return $this->glue_url($this->dest_url, $hide_password);
+  }
+
+  /**
+   * Glue a URLs component parts back into a URL.
+   */
+  public function glue_url($parts, $hide_password = TRUE) {
+    // Obscure the password if we need to.
+    $parts['pass'] = $hide_password ? "" : $parts['pass'];
+
+    // Assemble the URL.
+    $out = "";
+    $out .= $parts['scheme'] . '://';
+    $out .= $parts['user'] ? urlencode($parts['user']) : '';
+    $out .= ($parts['user'] && $parts['pass']) ? ":" . urlencode($parts['pass']) : '';
+    $out .= ($parts['user'] || $parts['pass']) ? "@" : "";
+    $out .= $parts['host'];
+    $out .= !empty($parts['port']) ? ':' . $parts['port'] : '';
+    $out .= "/" . $parts['path'];
+    return $out;
+  }
+
+  /**
+   * Break a URL into it's component parts.
+   */
+  public function set_url($url) {
+    $parts          = (array) parse_url($url);
+    $parts['user']  = urldecode(@$parts['user']);
+    $parts['pass']  = urldecode(@$parts['pass']);
+    $parts['path']  = urldecode(@$parts['path']);
+    $parts['path']  = ltrim(@$parts['path'], "/");
+    $this->dest_url = $parts;
+  }
+
+  /**
+   * Retrieve a list of filetypes supported by this source/destination.
+   */
+  public function file_types() {
+    return array();
+  }
+
+}
+
+/**
+ * A base class for creating locations.
+ */
+class backup_migrate_location_remote extends backup_migrate_location {
+
+  /**
+   * The location is a URI so parse it and store the parts.
+   */
+  public function get_location() {
+    return $this->url(FALSE);
+  }
+
+  /**
+   * The location to display is the url without the password.
+   */
+  public function get_display_location() {
+    return $this->url(TRUE);
+  }
+
+  /**
+   * Return the location with the password.
+   */
+  public function set_location($location) {
+    $this->location = $location;
+    $this->set_url($location);
+  }
+
+  /**
+   * Location configuration callback.
+   */
+  public function edit_form() {
+    $form = parent::edit_form();
+    $form['scheme'] = array(
+      "#type" => "select",
+      "#title" => t("Scheme"),
+      "#default_value" => @$this->dest_url['scheme'] ? $this->dest_url['scheme'] : 'mysql',
+      "#required" => TRUE,
+      "#options" => array($GLOBALS['db_type'] => $GLOBALS['db_type']),
+      "#weight" => 0,
+    );
+    $form['host'] = array(
+      "#type" => "textfield",
+      "#title" => t("Host"),
+      "#default_value" => @$this->dest_url['host'] ? $this->dest_url['host'] : 'localhost',
+      "#required" => TRUE,
+      "#weight" => 10,
+    );
+    $form['path'] = array(
+      "#type" => "textfield",
+      "#title" => t("Path"),
+      "#default_value" => @$this->dest_url['path'],
+      "#required" => TRUE,
+      "#weight" => 20,
+    );
+    $form['user'] = array(
+      "#type" => "textfield",
+      "#title" => t("Username"),
+      "#default_value" => @$this->dest_url['user'],
+      "#required" => TRUE,
+      "#weight" => 30,
+    );
+    $form['pass'] = array(
+      "#type" => "password",
+      "#title" => t("Password"),
+      "#default_value" => @$this->dest_url['pass'],
+      '#description' => '',
+      "#weight" => 40,
+    );
+    if (@$this->dest_url['pass']) {
+      $form['old_password'] = array(
+        "#type" => "value",
+        "#value" => @$this->dest_url['pass'],
+      );
+      $form['pass']["#description"] .= t(' You do not need to enter a password unless you wish to change the currently saved password.');
+    }
+    return $form;
+  }
+
+  /**
+   * Submit the configuration form. Glue the url together and add the old password back if a new one was not specified.
+   */
+  public function edit_form_submit($form, &$form_state) {
+    $form_state['values']['pass'] = $form_state['values']['pass'] ? $form_state['values']['pass'] : $form_state['values']['old_password'];
+    $form_state['values']['location'] = $this->glue_url($form_state['values'], FALSE);
+    parent::edit_form_submit($form, $form_state);
+  }
+
+}

+ 145 - 70
sites/all/modules/contrib/admin/backup_migrate/includes/profiles.inc

@@ -1,19 +1,35 @@
 <?php
 
-
 /**
  * @file
  * All of the settings profiles handling code for Backup and Migrate.
  */
 
-backup_migrate_include('crud');
+require_once dirname(__FILE__) . '/crud.inc';
+
+/**
+ * Implements hook_backup_migrate_profile_subtypes().
+ *
+ * Get the built in Backup and Migrate profile types.
+ */
+function backup_migrate_backup_migrate_profile_subtypes() {
+  $out = array(
+    'backup' => array(
+      'include' => 'profiles',
+      'type_name' => t('Backup Settings Profile'),
+      'class' => 'backup_migrate_profile',
+    ),
+  );
+  return $out;
+}
 
 /**
  * Get all the available backup profiles.
  */
 function backup_migrate_get_profiles() {
-  backup_migrate_include('filters');
-  static $profiles = NULL;
+  require_once dirname(__FILE__) . '/filters.inc';
+
+  $profiles = &drupal_static('backup_migrate_get_profiles', NULL);
 
   // Get the list of profiles and cache them locally.
   if ($profiles === NULL) {
@@ -23,14 +39,14 @@ function backup_migrate_get_profiles() {
 }
 
 /**
- * Implementation of hook_backup_migrate_profiles_alter().
+ * Implements hook_backup_migrate_profiles_alter().
  *
  * Add default settings for any plugins which didn't exist when the profile was saved.
  */
 function backup_migrate_backup_migrate_profiles_alter(&$profiles) {
   foreach ($profiles as $id => $profile) {
     // Set the default values for filter setting which don't exist in the profile.
-    $profiles[$id]->filters = (array)@$profile->filters + (array)backup_migrate_filters_settings_default('backup');
+    $profiles[$id]->filters = (array) @$profile->filters + (array) backup_migrate_filters_settings_default('backup');
   }
 }
 
@@ -43,13 +59,13 @@ function backup_migrate_get_profile($profile_id) {
 }
 
 /**
- * Implementation of hook_backup_migrate_profiles().
+ * Implements hook_backup_migrate_profiles().
  */
 function backup_migrate_backup_migrate_profiles() {
   $out = array();
 
   // Get the module default profile.
-  $out['default'] = backup_migrate_crud_create_item('profile', array('name' => t("Default Settings"), 'profile_id' => 'default'));
+  $out['default'] = backup_migrate_crud_create_item('profile', array('name' => t("Default Settings"), 'machine_name' => 'default'));
 
   return $out;
 }
@@ -61,7 +77,7 @@ function backup_migrate_backup_migrate_profiles() {
  */
 function _backup_migrate_get_profile_form_item_options() {
   $out = array();
-  foreach ((array)backup_migrate_get_profiles() as $key => $profile) {
+  foreach ((array) backup_migrate_get_profiles() as $key => $profile) {
     $out[$key] = $profile->get('name');
   }
   return $out;
@@ -71,13 +87,15 @@ function _backup_migrate_get_profile_form_item_options() {
  * Get a form to configure the profile.
  */
 function _backup_migrate_ui_backup_settings_form($profile) {
-  drupal_add_js(array('backup_migrate' => array('checkboxLinkText' => t('View as checkboxes'))), array('type' => 'setting'));
-  drupal_add_js(drupal_get_path('module', 'backup_migrate') .'/backup_migrate.js', array('type' => 'file', 'scope' => 'footer'));
-  drupal_add_css(drupal_get_path('module', 'backup_migrate') .'/backup_migrate.css');
+  require_once dirname(__FILE__) . '/destinations.inc';
+  require_once dirname(__FILE__) . '/files.inc';
+  require_once dirname(__FILE__) . '/filters.inc';
 
-  backup_migrate_include('files', 'destinations', 'filters');
+  drupal_add_js(array('backup_migrate' => array('checkboxLinkText' => t('View as checkboxes'))), array('type' => 'setting'));
+  drupal_add_js(drupal_get_path('module', 'backup_migrate') . '/backup_migrate.js', array('type' => 'file', 'scope' => 'footer'));
+  drupal_add_css(drupal_get_path('module', 'backup_migrate') . '/backup_migrate.css');
 
-  $form    = array();
+  $form = array();
 
   $form['file'] = array(
     "#type" => "fieldset",
@@ -92,8 +110,6 @@ function _backup_migrate_ui_backup_settings_form($profile) {
     "#default_value" => $profile->filename,
   );
   if (module_exists('token')) {
-    $form['file']['filename']['#description'] = t('You can use tokens in the file name.');
-
     $form['file']['token_help'] = array(
       '#title' => t('Replacement patterns'),
       '#type' => 'fieldset',
@@ -105,14 +121,26 @@ function _backup_migrate_ui_backup_settings_form($profile) {
       '#token_types' => array('current-date', 'site'),
       '#global_types' => FALSE,
     );
+    $form['file']['filename']['#description'] = t('You can use tokens in the file name.');
   }
 
   $form['file']['append_timestamp'] = array(
-    "#type" => "checkbox",
-    "#title" => t("Append a timestamp."),
+    "#type" => "radios",
+    '#options' => array(
+      0 => t('Create separate backups if `Backup file name` already exists'),
+      2 => t('Overwrite the existing backup file'),
+      1 => t('Append the timestamp'),
+    ),
+    "#title" => t("Save mode"),
     "#default_value" => $profile->append_timestamp,
   );
-  $form['file']['timestamp_format'] = array(
+  $form['file']['timestamp_format_wrapper'] = array(
+    '#type' => 'backup_migrate_dependent',
+    '#dependencies' => array(
+      'append_timestamp' => 1,
+    ),
+  );
+  $form['file']['timestamp_format_wrapper']['timestamp_format'] = array(
     "#type" => "textfield",
     "#title" => t("Timestamp format"),
     "#default_value" => $profile->timestamp_format,
@@ -126,12 +154,12 @@ function _backup_migrate_ui_backup_settings_form($profile) {
   if ($form['advanced']) {
     $form['advanced']['#type'] = 'fieldset';
     $form['advanced']['#title'] = t('Advanced Options');
-    $form['advanced']['#collapsed'] = true;
-    $form['advanced']['#collapsible'] = true;
+    $form['advanced']['#collapsed'] = TRUE;
+    $form['advanced']['#collapsible'] = TRUE;
   }
 
-  $form['#validate'][]  = '_backup_migrate_ui_backup_settings_form_validate';
-  $form['#submit'][]    = '_backup_migrate_ui_backup_settings_form_submit';
+  $form['#validate'][] = '_backup_migrate_ui_backup_settings_form_validate';
+  $form['#submit'][]   = '_backup_migrate_ui_backup_settings_form_submit';
 
   return $form;
 }
@@ -149,12 +177,14 @@ function _backup_migrate_ui_backup_settings_form_validate($form, &$form_state) {
 function _backup_migrate_ui_backup_settings_form_submit($form, &$form_state) {
   backup_migrate_filters_settings_form_submit('backup', $form, $form_state);
 }
-  
+
 /**
  * Get the default profile.
  */
 function _backup_migrate_profile_default_profile() {
-  backup_migrate_include('files', 'filters');
+  require_once dirname(__FILE__) . '/files.inc';
+  require_once dirname(__FILE__) . '/filters.inc';
+
   return array(
     'source_id' => 'db',
     'filename' => _backup_migrate_default_filename(),
@@ -183,41 +213,62 @@ function _backup_migrate_profile_saved_default_profile($profile_id = NULL) {
  * A profile class for crud operations.
  */
 class backup_migrate_profile extends backup_migrate_item {
-  var $db_table = "backup_migrate_profiles";
-  var $type_name = "profile";
-  var $singular = 'profile';
-  var $plural = 'profiles';
+  public $db_table = "backup_migrate_profiles";
+  public $type_name = "profile";
+  public $singular = 'settings profile';
+  public $plural = 'settings profiles';
+  public $title_plural = 'Settings Profiles';
+  public $title_singular = 'Settings Profile';
+
+  /**
+   * Perform a shallow merge of the defaults and the parameters.
+   *
+   * This is needed because otherwise it will *combine* the nested arrays and
+   * make it impossible to deselect database tables from the 'nodata' setting.
+   *
+   * @param array $params
+   */
+  public function __construct(array $params = array()) {
+    $params = (array) $params;
+    $defaults = (array) $this->get_default_values();
+    foreach ($defaults as $key => $val) {
+      if (!isset($params[$key])) {
+        $params[$key] = $val;
+      }
+    }
+    $this->from_array($params);
+  }
 
   /**
    * This function is not supposed to be called. It is just here to help the po extractor out.
    */
-  function strings() {
+  public function strings() {
     // Help the pot extractor find these strings.
-    t('Profile');
-    t('Profiles');
-    t('profile');
-    t('profiles');
+    t('Settings Profile');
+    t('Settings Profiles');
+    t('settings profile');
+    t('settings profiles');
   }
 
   /**
    * Get the default values for standard parameters.
    */
-  function get_default_values() {
+  public function get_default_values() {
     return _backup_migrate_profile_default_profile() + array('name' => t("Untitled Profile"));
   }
 
   /**
    * Get a table of all items of this type.
-   */  
-  function get_list() {
-    drupal_add_css(drupal_get_path('module', 'backup_migrate') .'/backup_migrate.css');
+   */
+  public function get_list() {
+    drupal_add_css(drupal_get_path('module', 'backup_migrate') . '/backup_migrate.css');
     return parent::get_list();
   }
 
   /**
    * Get the columns needed to list the type.
-   */  
-  function get_list_column_info() {
+   */
+  public function get_list_column_info() {
     $out = parent::get_list_column_info();
     $out = array(
       'name'                  => array('title' => t('Name')),
@@ -227,23 +278,10 @@ class backup_migrate_profile extends backup_migrate_item {
     return $out;
   }
 
-  /**
-   * Get a row of data to be used in a list of items of this type.
-   */  
-  function get_list_row() {
-    $row = parent::get_list_row();
-    if (empty($this->enabled)) {
-      foreach ($row as $key => $field) {
-        $row[$key] = array('data' => $field, 'class' => 'profile-list-disabled');
-      }
-    }
-    return $row;
-  }
-
   /**
    * Set the source of this setings profile. Takes either a source object or source id.
    */
-  function set_source($source) {
+  public function set_source($source) {
     if (is_object($source)) {
       $this->source = $source;
       $this->source_id = $source->get_id();
@@ -257,10 +295,11 @@ class backup_migrate_profile extends backup_migrate_item {
   /**
    * Get the source of the profile.
    */
-  function get_source() {
-    backup_migrate_include('destinations');
-    if (!empty($this->source_id) && (empty($this->source) || $this->source->destination_id !== $this->source_id)) {
-      $this->source = backup_migrate_get_destination($this->source_id);
+  public function get_source() {
+    require_once dirname(__FILE__) . '/locations.inc';
+
+    if (!empty($this->source_id) && (empty($this->source) || $this->source->get_id() !== $this->source_id)) {
+      $this->source = backup_migrate_get_source($this->source_id);
     }
     return empty($this->source) ? NULL : $this->source;
   }
@@ -268,7 +307,7 @@ class backup_migrate_profile extends backup_migrate_item {
   /**
    * Get the name of the source.
    */
-  function get_source_name() {
+  public function get_source_name() {
     if ($source = $this->get_source()) {
       return $source->get_name();
     }
@@ -278,28 +317,64 @@ class backup_migrate_profile extends backup_migrate_item {
   /**
    * Get the destination of the profile.
    */
-  function get_destination() {
-    backup_migrate_include('destinations');
-    if (!empty($this->destination_id) && (empty($this->destination) || $this->destination->destination_id !== $this->destination_id)) {
-      $this->destination = backup_migrate_get_destination($this->destination_id);
+  public function get_destination() {
+    $destinations = (array) $this->get_destinations();
+    return reset($destinations);
+  }
+
+  /**
+   * Get the destination of the profile.
+   */
+  public function get_destinations() {
+    require_once dirname(__FILE__) . '/destinations.inc';
+
+    if (empty($this->destinations)) {
+      $this->destinations = array();
+      $ids = $weights = array();
+      if (!empty($this->destination_id)) {
+        foreach ((array) $this->destination_id as $destination_id) {
+          if (!in_array($destination_id, $ids) && $destination = backup_migrate_get_destination($destination_id)) {
+            $this->destinations[] = $destination;
+            $weights[] = $destination->get('weight');
+            $ids[] = $destination_id;
+          }
+        }
+      }
+      // Sort the destinations by weight.
+      array_multisort($weights, SORT_NUMERIC, $this->destinations);
     }
-    return empty($this->destination) ? NULL : $this->destination;
+
+    return $this->destinations;
   }
 
   /**
    * Get the name of the destination.
    */
-  function get_destination_name() {
-    if ($destination = $this->get_destination()) {
-      return $destination->get_name();
+  public function get_destination_name() {
+    $out = array();
+    foreach ($this->get_destinations() as $destination) {
+      $out[] = $destination->get_name();
+    }
+    if ($out) {
+      return implode(', ', $out);
     }
     return t("Missing");
   }
 
+  /**
+   * Get the source and destinations specified in the given settings profile.
+   */
+  public function get_all_locations() {
+    $out = array();
+    $out += $this->get('destinations');
+    $out[] = $this->get('source');
+    return $out;
+  }
+
   /**
    * Get the edit form.
    */
-  function edit_form() {
+  public function edit_form() {
     $form = parent::edit_form();
     $form['name'] = array(
       "#type" => "textfield",
@@ -314,8 +389,8 @@ class backup_migrate_profile extends backup_migrate_item {
   /**
    * Get the message to send to the user when confirming the deletion of the item.
    */
-  function delete_confirm_message() {
+  public function delete_confirm_message() {
     return t('Are you sure you want to delete the profile %name? Any schedules using this profile will be disabled.', array('%name' => $this->get('name')));
   }
-}
 
+}

+ 577 - 118
sites/all/modules/contrib/admin/backup_migrate/includes/schedules.inc

@@ -5,24 +5,82 @@
  * All of the schedule handling code needed for Backup and Migrate.
  */
 
-backup_migrate_include('crud');
+define('BACKUP_MIGRATE_KEEP_ALL', 0);
+define('BACKUP_MIGRATE_STANDARD_DELETE', -1);
+define('BACKUP_MIGRATE_SMART_DELETE', -2);
+
+define('BACKUP_MIGRATE_CRON_BUILTIN', 'builtin');
+define('BACKUP_MIGRATE_CRON_ELYSIA', 'elysia');
+define('BACKUP_MIGRATE_CRON_NONE', 'none');
+
+
+define('BACKUP_MIGRATE_SMART_KEEP_SUBHOURLY', 3600);
+define('BACKUP_MIGRATE_SMART_KEEP_HOURLY', 24);
+define('BACKUP_MIGRATE_SMART_KEEP_DAILY', 30);
+define('BACKUP_MIGRATE_SMART_KEEP_WEEKLY', PHP_INT_MAX);
+
+define('BACKUP_MIGRATE_KEEP_DEFAULT', 100);
+
+require_once dirname(__FILE__) . '/crud.inc';
+
+/**
+ * Implements hook_backup_migrate_destination_types().
+ *
+ * Get the built in Backup and Migrate destination types.
+ */
+function backup_migrate_backup_migrate_schedule_types() {
+  $out = array();
+  $out += array(
+    'backup' => array(
+      'include' => 'schedule',
+      'type_name' => t('Backup Schedule'),
+      'class' => 'backup_migrate_schedule',
+    ),
+  );
+  return $out;
+}
 
 /**
  * Run the preconfigured schedules. Called on cron.
  */
-function backup_migrate_schedules_run() {
-  backup_migrate_include('profiles');
+function backup_migrate_schedules_cron() {
+  require_once dirname(__FILE__) . '/profiles.inc';
+
   foreach (backup_migrate_get_schedules() as $schedule) {
     $schedule->cron();
   }
   backup_migrate_cleanup();
 }
 
+/**
+ * Run the preconfigured schedules regardless of scheduled time settings.
+ */
+function backup_migrate_schedules_run() {
+  require_once dirname(__FILE__) . '/profiles.inc';
+
+  foreach (backup_migrate_get_schedules() as $schedule) {
+    $schedule->run();
+  }
+  backup_migrate_cleanup();
+}
+
+/**
+ * Run the preconfigured schedules. Called on cron.
+ */
+function backup_migrate_schedule_run($schedule_id) {
+  require_once dirname(__FILE__) . '/profiles.inc';
+
+  if (($schedule = backup_migrate_get_schedule($schedule_id)) && $schedule->is_enabled()) {
+    $schedule->run();
+  }
+  backup_migrate_cleanup();
+}
+
 /**
  * Get all the available backup schedules.
  */
 function backup_migrate_get_schedules() {
-  static $schedules = NULL;
+  $schedules = &drupal_static('backup_migrate_get_schedules');
   // Get the list of schedules and cache them locally.
   if ($schedules === NULL) {
     $schedules = backup_migrate_crud_get_items('schedule');
@@ -42,16 +100,18 @@ function backup_migrate_get_schedule($schedule_id) {
  * A schedule class for crud operations.
  */
 class backup_migrate_schedule extends backup_migrate_item {
-  var $db_table = "backup_migrate_schedules";
-  var $type_name = 'schedule';
-  var $singular = 'schedule';
-  var $plural = 'schedules';
-  var $default_values = array();
+  public $db_table = "backup_migrate_schedules";
+  public $type_name = 'schedule';
+  public $singular = 'schedule';
+  public $plural = 'schedules';
+  public $title_plural = 'Schedules';
+  public $title_singular = 'Schedule';
+  public $default_values = array();
 
   /**
    * This function is not supposed to be called. It is just here to help the po extractor out.
    */
-  function strings() {
+  public function strings() {
     // Help the pot extractor find these strings.
     t('Schedule');
     t('Schedules');
@@ -62,25 +122,36 @@ class backup_migrate_schedule extends backup_migrate_item {
   /**
    * Get the default values for this item.
    */
-  function get_default_values() {
+  public function get_default_values() {
     return array(
-        'name' => t("Untitled Schedule"),
-        'source_id' => 'db',
-        'enabled' => 1,
-        'keep' => 0,
-        'period' => 60 * 60 * 24,
-        'storage' => BACKUP_MIGRATE_STORAGE_NONE
-      );
+      'name' => t("Untitled Schedule"),
+      'source_id' => 'db',
+      'enabled' => 1,
+      'keep' => BACKUP_MIGRATE_KEEP_ALL,
+      'period' => 60 * 60 * 24,
+      'storage' => BACKUP_MIGRATE_STORAGE_NONE,
+      'cron' => BACKUP_MIGRATE_CRON_BUILTIN,
+      'cron_schedule' => '0 4 * * *',
+    );
+  }
+
+  /**
+   * Return as an array of values.
+   */
+  public function to_array() {
+    $out = parent::to_array();
+    unset($out['last_run']);
+    return $out;
   }
 
   /**
    * Get the columns needed to list the type.
-   */  
-  function get_list_column_info() {
+   */
+  public function get_list_column_info() {
     $out = parent::get_list_column_info();
     $out = array(
       'name'                  => array('title' => t('Name')),
-      'destination_name'      => array('title' => t('Destination'), 'html' => TRUE),
+      'destination_name'      => array('title' => t('Destinations'), 'html' => TRUE),
       'profile_name'          => array('title' => t('Profile'), 'html' => TRUE),
       'frequency_description' => array('title' => t('Frequency')),
       'keep_description'      => array('title' => t('Keep')),
@@ -90,15 +161,40 @@ class backup_migrate_schedule extends backup_migrate_item {
     return $out;
   }
 
+  /**
+   * Get the columns needed to list the type.
+   */
+  public function get_settings_path() {
+    // Pull the schedules tab up a level to the top.
+    return BACKUP_MIGRATE_MENU_PATH . '/' . $this->type_name;
+  }
+
+  /**
+   * Get the menu items for manipulating this type.
+   */
+  public function get_menu_items() {
+    $items = parent::get_menu_items();
+    $path = $this->get_settings_path();
+    return $items;
+  }
+
   /**
    * Get a row of data to be used in a list of items of this type.
    */
-  function get_list_row() {
-    drupal_add_css(drupal_get_path('module', 'backup_migrate') .'/backup_migrate.css');
+  public function get_list_row() {
+    drupal_add_css(drupal_get_path('module', 'backup_migrate') . '/backup_migrate.css');
     $row = parent::get_list_row();
     if (!$this->is_enabled()) {
       foreach ($row as $key => $field) {
-        $row[$key] = array('data' => $field, 'class' => 'schedule-list-disabled');
+        if (!is_array($field)) {
+          $row[$key] = array('data' => $field, 'class' => 'schedule-list-disabled');
+        }
+        elseif (isset($row[$key]['class'])) {
+          $row[$key]['class'] .= ' schedule-list-disabled';
+        }
+        else {
+          $row[$key]['class'] = 'schedule-list-disabled';
+        }
       }
     }
     return $row;
@@ -107,7 +203,7 @@ class backup_migrate_schedule extends backup_migrate_item {
   /**
    * Is the schedule enabled and valid.
    */
-  function is_enabled() {
+  public function is_enabled() {
     $destination = $this->get_destination();
     $profile = $this->get_profile();
     return (!empty($this->enabled) && !empty($destination) && !empty($profile));
@@ -116,66 +212,167 @@ class backup_migrate_schedule extends backup_migrate_item {
   /**
    * Get the destination object of the schedule.
    */
-  function get_destination() {
-    backup_migrate_include('destinations');
-    return backup_migrate_get_destination($this->get('destination_id'));
+  public function get_destination() {
+    $destinations = (array) $this->get_destinations();
+    return reset($destinations);
+  }
+
+  /**
+   * Get the destination object of the schedule.
+   */
+  public function get_destination_ids() {
+    $out = array();
+    foreach (array('destination_id', 'copy_destination_id') as $key) {
+      if ($id = $this->get($key)) {
+        $out[$key] = $id;
+      }
+    }
+    return $out;
+  }
+
+  /**
+   * Get the destination object of the schedule.
+   */
+  public function get_destinations() {
+    require_once dirname(__FILE__) . '/destinations.inc';
+
+    $out = array();
+    foreach ($this->get_destination_ids() as $id) {
+      if ($dest = backup_migrate_get_destination($id)) {
+        $out[$id] = $dest;
+      }
+    }
+    return $out;
+  }
+
+  /**
+   * Get the destination object of the schedule.
+   */
+  public function get_destination_remote() {
+    require_once dirname(__FILE__) . '/destinations.inc';
+
+    return backup_migrate_get_destination($this->get('destination_remote_id'));
+  }
+
+  /**
+   * Get the destination object of the schedule.
+   */
+  public function get_destination_local() {
+    require_once dirname(__FILE__) . '/destinations.inc';
+
+    return backup_migrate_get_destination($this->get('destination_local_id'));
   }
 
   /**
    * Get the name of the destination.
    */
-  function get_destination_name() {
-    if ($destination = $this->get_destination()) {
-      return check_plain($destination->get_name());
+  public function get_destination_name() {
+    if ($destinations = $this->get_destinations()) {
+      $out = array();
+      foreach ((array) $destinations as $destination) {
+        $out[] = check_plain($destination->get_name());
+      }
+      return implode(', ', $out);
     }
-    return '<div class="row-error">'. t("Missing") .'</div>';
+    return '<div class="row-error">' . t("Missing") . '</div>';
   }
 
   /**
    * Get the destination of the schedule.
    */
-  function get_profile() {
-    backup_migrate_include('profiles');
-    return backup_migrate_get_profile($this->get('profile_id'));
+  public function get_profile() {
+    require_once dirname(__FILE__) . '/profiles.inc';
+
+    if ($settings = backup_migrate_get_profile($this->get('profile_id'))) {
+      $settings->file_info = empty($settings->file_info) ? array() : $settings->file_info;
+      $settings->file_info += array(
+        'schedule_id'   => $this->get_id(),
+        'schedule_name' => $this->get('name'),
+      );
+    }
+    return $settings;
+
   }
 
   /**
    * Get the name of the source.
    */
-  function get_profile_name() {
+  public function get_profile_name() {
     if ($profile = $this->get_profile()) {
       return check_plain($profile->get_name());
     }
-    return '<div class="row-error">'. t("Missing") .'</div>';
+    return '<div class="row-error">' . t("Missing") . '</div>';
   }
 
   /**
    * Format a frequency in human-readable form.
    */
-  function get_frequency_description() {
+  public function get_frequency_description() {
     $period = $this->get_frequency_period();
-    $out = format_plural(($this->period / $period['seconds']), $period['singular'], $period['plural']);
+    $cron = $this->get('cron');
+    if ($cron == BACKUP_MIGRATE_CRON_BUILTIN) {
+      $out = format_plural(($this->period / $period['seconds']), $period['singular'], $period['plural']);
+    }
+    elseif ($cron == BACKUP_MIGRATE_CRON_ELYSIA) {
+      $out = $this->get('cron_schedule');
+    }
+    else {
+      $out = t('None');
+    }
     return $out;
   }
 
   /**
    * Format the number to keep in human-readable form.
    */
-  function get_keep_description() {
-    return !empty($this->keep) ? $this->keep : t('All');
+  public function get_keep_description() {
+    return $this->generate_keep_description($this->keep);
+  }
+
+  /**
+   * Format a number to keep in human readable from.
+   */
+  public function generate_keep_description($keep, $terse = TRUE) {
+    if ($keep == BACKUP_MIGRATE_KEEP_ALL) {
+      return t('all backups');
+    }
+    elseif ($keep == BACKUP_MIGRATE_SMART_DELETE) {
+      $keep_hourly = variable_get('backup_migrate_smart_keep_hourly', BACKUP_MIGRATE_SMART_KEEP_HOURLY);
+      $keep_daily  = variable_get('backup_migrate_smart_keep_daily', BACKUP_MIGRATE_SMART_KEEP_DAILY);
+      $keep_weekly = variable_get('backup_migrate_smart_keep_weekly', BACKUP_MIGRATE_SMART_KEEP_WEEKLY);
+      if ($terse) {
+        return t('!hours hourly, !days daily, !weeks weekly backups',
+          array(
+            '!hours' => $keep_hourly == PHP_INT_MAX ? t('all') : $keep_hourly,
+            '!days'  => $keep_daily == PHP_INT_MAX ? t('all') : $keep_daily,
+            '!weeks' => $keep_weekly == PHP_INT_MAX ? t('all') : $keep_weekly,
+          ));
+      }
+      else {
+        return t('hourly backups !hours, daily backups !days and weekly backups !weeks',
+          array(
+            '!hours' => $keep_hourly == PHP_INT_MAX ? t('forever') : format_plural($keep_hourly, 'for 1 hour', 'for the past @count hours'),
+            '!days'  => $keep_daily == PHP_INT_MAX ? t('forever') : format_plural($keep_daily, 'for 1 day', 'for the past @count days'),
+            '!weeks' => $keep_weekly == PHP_INT_MAX ? t('forever') : format_plural($keep_weekly, 'for 1 week', 'for the past @count weeks'),
+          )
+        );
+      }
+    }
+
+    return format_plural($keep, 'last 1 backup', 'last @count backups');
   }
 
   /**
    * Format the enabled status in human-readable form.
    */
-  function get_enabled_description() {
+  public function get_enabled_description() {
     return !empty($this->enabled) ? t('Enabled') : t('Disabled');
   }
 
   /**
    * Format the enabled status in human-readable form.
    */
-  function get_last_run_description() {
+  public function get_last_run_description() {
     $last_run = $this->get('last_run');
     return !empty($last_run) ? format_date($last_run, 'small') : t('Never');
   }
@@ -183,29 +380,27 @@ class backup_migrate_schedule extends backup_migrate_item {
   /**
    * Get the number of excluded tables.
    */
-  function get_exclude_tables_count() {
+  public function get_exclude_tables_count() {
     return count($this->exclude_tables) ? count($this->exclude_tables) : t("No tables excluded");
   }
 
   /**
    * Get the number of excluded tables.
    */
-  function get_nodata_tables_count() {
+  public function get_nodata_tables_count() {
     return count($this->nodata_tables) ? count($this->nodata_tables) : t("No data omitted");
   }
 
   /**
    * Get the edit form.
    */
-  function edit_form() {
+  public function edit_form() {
+    require_once dirname(__FILE__) . '/destinations.inc';
+    require_once dirname(__FILE__) . '/sources.inc';
+    require_once dirname(__FILE__) . '/profiles.inc';
+
     $form = parent::edit_form();
-    backup_migrate_include('destinations', 'profiles');
 
-    $form['enabled'] = array(
-      "#type" => "checkbox",
-      "#title" => t("Enabled"),
-      "#default_value" => $this->get('enabled'),
-    );
     $form['name'] = array(
       "#type" => "textfield",
       "#title" => t("Schedule Name"),
@@ -220,7 +415,7 @@ class backup_migrate_schedule extends backup_migrate_item {
       "#options" => _backup_migrate_get_profile_form_item_options(),
       "#default_value" => $this->get('profile_id'),
     );
-    $form['profile_id']['#description'] = ' '. l(t("Create new profile"), BACKUP_MIGRATE_MENU_PATH . "/profile/add");
+    $form['profile_id']['#description'] = ' ' . l(t('Create new profile'), BACKUP_MIGRATE_MENU_PATH . '/settings/profile/add');
     if (!$form['profile_id']['#options']) {
       $form['profile_id']['#options'] = array('0' => t('-- None Available --'));
     }
@@ -232,40 +427,138 @@ class backup_migrate_schedule extends backup_migrate_item {
     $default_period     = $this->get_frequency_period();
     $default_period_num = $this->get('period') / $default_period['seconds'];
 
-    $form['period']     = array(
+    $form['enabled'] = array(
+      '#type' => "checkbox",
+      '#title' => t('Enabled'),
+      '#default_value' => $this->get('enabled'),
+    );
+    $form['cron_settings'] = array(
+      '#type' => 'backup_migrate_dependent',
+      '#dependencies' => array(
+        'enabled' => TRUE,
+      ),
+    );
+
+    $cron = $this->get('cron');
+    $form['cron_settings']['cron_builtin'] = array(
+      "#type" => "radio",
+      "#title" => t('Run using Drupal\'s cron'),
+      '#return_value' => BACKUP_MIGRATE_CRON_BUILTIN,
+      "#description" => t('Run this schedule when !cron runs.', array('!cron' => l(t('your cron task'), 'http://drupal.org/cron'))),
+      "#default_value" => $cron ? $cron : BACKUP_MIGRATE_CRON_BUILTIN,
+      '#parents' => array('cron'),
+    );
+
+    $form['cron_settings']['period_settings'] = array(
+      '#type' => 'backup_migrate_dependent',
+      '#dependencies' => array(
+        'cron' => BACKUP_MIGRATE_CRON_BUILTIN,
+      ),
+    );
+    $form['cron_settings']['period_settings']['period'] = array(
       "#type" => "item",
       "#title" => t("Backup every"),
       "#prefix" => '<div class="container-inline">',
       "#suffix" => '</div>',
       "#tree" => TRUE,
+      '#parents' => array('period'),
     );
-    $form['period']['number'] = array(
+    $form['cron_settings']['period_settings']['period']['number'] = array(
       "#type" => "textfield",
       "#size" => 6,
       "#default_value" => $default_period_num,
+      '#parents' => array('period', 'number'),
     );
-    $form['period']['type'] = array(
+    $form['cron_settings']['period_settings']['period']['type'] = array(
       "#type" => "select",
       "#options" => $period_options,
       "#default_value" => $default_period['type'],
+      '#parents' => array('period', 'type'),
     );
 
-    $form['keep'] = array(
+    $form['cron_settings']['cron_elysia'] = array(
+      "#type" => "radio",
+      "#title" => t('Run using Elysia cron'),
+      '#return_value' => BACKUP_MIGRATE_CRON_ELYSIA,
+      "#description" => t('You can specify exactly when this schedule should run using !elysia.', array('!elysia' => l(t('the Elysia Cron module'), 'http://drupal.org/project/elysia_cron'))),
+      "#default_value" => $cron ? $cron : BACKUP_MIGRATE_CRON_BUILTIN,
+      '#parents' => array('cron'),
+    );
+    if (!module_exists('elysia_cron') && !module_exists('ultimate_cron')) {
+      $form['cron_settings']['cron_elysia']['#disabled'] = TRUE;
+      $form['cron_settings']['cron_elysia']['#description'] .= ' ' . t('Install !elysia to enable this option.', array('!elysia' => l(t('Elysia Cron'), 'http://drupal.org/project/elysia_cron')));
+    }
+    $form['cron_settings']['cron_schedule_settings'] = array(
+      '#type' => 'backup_migrate_dependent',
+      '#dependencies' => array(
+        'cron' => BACKUP_MIGRATE_CRON_ELYSIA,
+      ),
+    );
+    $form['cron_settings']['cron_schedule_settings']['cron_schedule'] = array(
+      "#type" => "textfield",
+      "#title" => t('Cron Schedule'),
+      '#length' => 10,
+      "#description" => t('Specify the frequency of the schedule using standard cron notation. For more information see the !elysiareadme.', array('!elysiareadme' => l(t('the Elysia Cron README'), 'http://drupalcode.org/project/elysia_cron.git/blob/refs/heads/7.x-1.x:/README.txt'))),
+      "#default_value" => $this->get('cron_schedule'),
+      '#parents' => array('cron_schedule'),
+    );
+
+    $form['cron_settings']['cron_none'] = array(
+      "#type" => "radio",
+      "#title" => t('Do not run automatically'),
+      '#return_value' => 'none',
+      "#description" => t('Do not run this schedule automatically. You can still run it using !drush.', array('!drush' => l(t('Drush'), 'http://drupal.org/project/drush'))),
+      "#default_value" => $cron ? $cron : BACKUP_MIGRATE_CRON_BUILTIN,
+      '#parents' => array('cron'),
+    );
+
+    $keep = $this->get('keep');
+    $form['delete'] = array(
+      '#type' => 'checkbox',
+      '#default_value' => $keep != 0,
+      '#title' => t('Automatically delete old backups'),
+    );
+    $form['delete_settings'] = array(
+      '#type' => 'backup_migrate_dependent',
+      '#dependencies' => array(
+        'delete' => TRUE,
+      ),
+    );
+
+    $keep_hourly = variable_get('backup_migrate_smart_keep_hourly', BACKUP_MIGRATE_SMART_KEEP_HOURLY);
+    $keep_daily = variable_get('backup_migrate_smart_keep_daily', BACKUP_MIGRATE_SMART_KEEP_DAILY);
+    $keep_weekly = variable_get('backup_migrate_smart_keep_weekly', BACKUP_MIGRATE_SMART_KEEP_WEEKLY);
+    $form['delete_settings']['smartdelete'] = array(
+      "#type" => "radio",
+      "#title" => t('Smart Delete'),
+      '#return_value' => BACKUP_MIGRATE_SMART_DELETE,
+      "#description" => t('Keep !keep. <strong>Recommended</strong>', array('!keep' => $this->generate_keep_description(BACKUP_MIGRATE_SMART_DELETE, FALSE))),
+      "#default_value" => $keep ? $keep : BACKUP_MIGRATE_SMART_DELETE,
+      '#parents' => array('deletetype'),
+    );
+    $form['delete_settings']['standarddelete'] = array(
+      "#type" => "radio",
+      "#title" => t('Simple Delete'),
+      '#return_value' => BACKUP_MIGRATE_STANDARD_DELETE,
+      "#description" => t("Keep a specified number of files deleting the oldest ones first."),
+      "#default_value" => $keep > 0 ? BACKUP_MIGRATE_STANDARD_DELETE : 0,
+      '#parents' => array('deletetype'),
+    );
+    $form['delete_settings']['keep-settings'] = array(
+      '#type' => 'backup_migrate_dependent',
+      '#dependencies' => array(
+        'deletetype' => BACKUP_MIGRATE_STANDARD_DELETE,
+      ),
+    );
+    $form['delete_settings']['keep-settings']['keep'] = array(
       "#type" => "textfield",
       "#size" => 6,
       "#title" => t("Number of Backup files to keep"),
-      "#description" => t("The number of backup files to keep before deleting old ones. Use 0 to never delete backups. <strong>Other files in the destination directory will get deleted if you specify a limit.</strong>"),
-      "#default_value" => $this->get('keep'),
-    );
-    $destination_options = _backup_migrate_get_destination_form_item_options('scheduled backup');
-    $form['destination_id'] = array(
-      "#type" => "select",
-      "#title" => t("Destination"),
-      "#description" => t("Choose where the backup file will be saved. Backup files contain sensitive data, so be careful where you save them."),
-      "#options" => $destination_options,
-      "#default_value" => $this->get('destination_id'),
+      "#description" => t("The number of backup files to keep before deleting old ones."),
+      "#default_value" => $keep > 0 ? $keep : BACKUP_MIGRATE_KEEP_DEFAULT,
     );
-    $form['destination_id']['#description'] .= ' '. l(t("Create new destination"), BACKUP_MIGRATE_MENU_PATH . "/destination/add");
+
+    $form['destination'] = _backup_migrate_get_destination_pulldown('scheduled backup', $this->get('destination_id'), $this->get('copy_destination_id'));
 
     return $form;
   }
@@ -273,12 +566,19 @@ class backup_migrate_schedule extends backup_migrate_item {
   /**
    * Submit the edit form.
    */
-  function edit_form_validate($form, &$form_state) {
+  public function edit_form_validate($form, &$form_state) {
     if (!is_numeric($form_state['values']['period']['number']) || $form_state['values']['period']['number'] <= 0) {
       form_set_error('period][number', t('Backup period must be a number greater than 0.'));
     }
-    if (!is_numeric($form_state['values']['keep']) || $form_state['values']['keep'] < 0) {
-      form_set_error('keep', t('Number to keep must be an integer greater than or equal to 0.'));
+
+    if (!$form_state['values']['delete']) {
+      $form_state['values']['keep'] = 0;
+    }
+    elseif ($form_state['values']['deletetype'] == BACKUP_MIGRATE_SMART_DELETE) {
+      $form_state['values']['keep'] = BACKUP_MIGRATE_SMART_DELETE;
+    }
+    elseif (!is_numeric($form_state['values']['keep']) || $form_state['values']['keep'] <= 0) {
+      form_set_error('keep', t('Number to keep must be a number greater than 0.'));
     }
     parent::edit_form_validate($form, $form_state);
   }
@@ -286,7 +586,7 @@ class backup_migrate_schedule extends backup_migrate_item {
   /**
    * Submit the edit form.
    */
-  function edit_form_submit($form, &$form_state) {
+  public function edit_form_submit($form, &$form_state) {
     $periods = $this->frequency_periods();
     $period = $periods[$form_state['values']['period']['type']];
     $form_state['values']['period'] = $form_state['values']['period']['number'] * $period['seconds'];
@@ -294,9 +594,9 @@ class backup_migrate_schedule extends backup_migrate_item {
   }
 
   /**
-   * Get the period of the frequency (ie: seconds, minutes etc.)
+   * Get the period of the frequency (ie: seconds, minutes etc.).
    */
-  function get_frequency_period() {
+  public function get_frequency_period() {
     foreach (array_reverse($this->frequency_periods()) as $period) {
       if ($period['seconds'] && ($this->period % $period['seconds']) === 0) {
         return $period;
@@ -308,7 +608,7 @@ class backup_migrate_schedule extends backup_migrate_item {
    * Get a list of available backup periods. Only returns time periods which have a
    *  (reasonably) consistent number of seconds (ie: no months).
    */
-  function frequency_periods() {
+  public function frequency_periods() {
     return array(
       'seconds' => array('type' => 'seconds', 'seconds' => 1, 'title' => t('Seconds'), 'singular' => t('Once a second'), 'plural' => t('Every @count seconds')),
       'minutes' => array('type' => 'minutes', 'seconds' => 60, 'title' => t('Minutes'), 'singular' => t('Once a minute'), 'plural' => t('Every @count minutes')),
@@ -321,83 +621,242 @@ class backup_migrate_schedule extends backup_migrate_item {
   /**
    * Get the message to send to the user when confirming the deletion of the item.
    */
-  function delete_confirm_message() {
-    return t('Are you sure you want to delete the profile %name? Any schedules using this profile will be disabled.', array('%name' => $this->get('name')));
+  public function delete_confirm_message() {
+    return t('Are you sure you want to delete the schedule %name? Backups made with this schedule will not be deleted.', array('%name' => $this->get('name')));
   }
 
   /**
    * Perform the cron action. Run the backup if enough time has elapsed.
    */
-  function cron() {
+  public function cron() {
     $now = time();
 
     // Add a small negative buffer (1% of the entire period) to the time to account for slight difference in cron run length.
-    $wait_time = $this->period - ($this->period * variable_get('backup_migrate_schedule_buffer', 0.01));
-
-    if ($this->is_enabled() && ($now - $this->get('last_run')) >= $wait_time) {
-      if ($settings = $this->get_profile()) {
-        $settings->destination_id = $this->destination_id;
-        $settings->source_id = $this->source_id;
-        backup_migrate_perform_backup($settings);
-        $this->update_last_run($now);
-        $this->remove_expired_backups();
-      }
-      else {
-        backup_migrate_backup_fail("Schedule '%schedule' could not be run because requires a profile which is missing.", array('%schedule' => $schedule->get_name()), $settings);
-      }
+    $wait_time = $this->period - ($this->period * variable_get('backup_migrate_schedule_buffer', BACKUP_MIGRATE_SCHEDULE_BUFFER));
+
+    $cron = $this->get('cron');
+    if ($cron == BACKUP_MIGRATE_CRON_BUILTIN && $this->is_enabled() && ($now - $this->get('last_run')) >= $wait_time) {
+      $this->run();
+    }
+  }
+
+  /**
+   * Run the actual schedule.
+   */
+  public function run() {
+    // Clear cached profile data as it could have been altered by a previous
+    // schedule run.
+    drupal_static_reset('backup_migrate_get_profiles');
+
+    if ($settings = $this->get_profile()) {
+      $settings->source_id = $this->get('source_id');
+      $settings->destination_id = $this->get('destination_ids');
+
+      $this->update_last_run(time());
+      backup_migrate_perform_backup($settings);
+      $this->remove_expired_backups();
+    }
+    else {
+      backup_migrate_backup_fail("Schedule '%schedule' could not be run because requires a profile which is missing.", array('%schedule' => $schedule->get_name()), $settings);
     }
   }
 
   /**
    * Set the last run time of a schedule to the given timestamp, or now if none specified.
    */
-  function update_last_run($timestamp = NULL) {
+  public function update_last_run($timestamp = NULL) {
     if ($timestamp === NULL) {
       $timestamp = time();
     }
     variable_set('backup_migrate_schedule_last_run_' . $this->get('id'), $timestamp);
   }
 
-/**
+  /**
    * Set the last run time of a schedule to the given timestamp, or now if none specified.
    */
-  function get_last_run($timestamp = NULL) {
+  public function get_last_run() {
     return variable_get('backup_migrate_schedule_last_run_' . $this->get('id'), 0);
   }
 
-
   /**
    * Remove older backups keeping only the number specified by the aministrator.
    */
-  function remove_expired_backups() {
-    backup_migrate_include('destinations');
+  public function remove_expired_backups() {
+    require_once dirname(__FILE__) . '/destinations.inc';
 
     $num_to_keep = $this->keep;
     // If num to keep is not 0 (0 is infinity).
-    if ($num_to_keep && ($destination = $this->get_destination())) {
-      $i = 0;
-      if ($destination->op('delete') && $destination_files = $destination->list_files()) {
-        // Sort the files by modified time.
-        foreach ($destination_files as $file) {
-          if ($file->is_recognized_type() && $destination->can_delete_file($file->file_id())) {
-            $files[str_pad($file->info('filetime'), 10, "0", STR_PAD_LEFT) ."-". $i++] = $file;
-          }
+    foreach ((array) $this->get_destinations() as $destination) {
+      if ($destination && $destination->op('delete') && $destination_files = $destination->list_files()) {
+        if ($num_to_keep == BACKUP_MIGRATE_SMART_DELETE) {
+          $this->smart_delete_backups(
+            $destination,
+            $destination_files,
+            variable_get('backup_migrate_smart_keep_subhourly', BACKUP_MIGRATE_SMART_KEEP_SUBHOURLY),
+            variable_get('backup_migrate_smart_keep_hourly', BACKUP_MIGRATE_SMART_KEEP_HOURLY),
+            variable_get('backup_migrate_smart_keep_daily', BACKUP_MIGRATE_SMART_KEEP_DAILY),
+            variable_get('backup_migrate_smart_keep_weekly', BACKUP_MIGRATE_SMART_KEEP_WEEKLY)
+          );
+        }
+        elseif ($num_to_keep != BACKUP_MIGRATE_KEEP_ALL) {
+          $this->delete_backups($destination, $destination_files, $num_to_keep);
+        }
+      }
+    }
+  }
+
+  /**
+   * Remove older backups keeping only the number specified by the aministrator.
+   */
+  public function delete_backups($destination, $files, $num_to_keep) {
+    require_once dirname(__FILE__) . '/destinations.inc';
+
+    $num_to_keep = $this->keep;
+
+    // Sort the files by modified time.
+    $i = 0;
+    foreach ($files as $id => $file) {
+      if ($file->is_recognized_type()) {
+        $time = $file->info('filetime');
+        $sorted[$id] = $time;
+      }
+    }
+    asort($sorted);
+
+    // If we are beyond our limit, remove as many as we need.
+    $num_files = count($files);
+
+    if ($num_files > $num_to_keep) {
+      $num_to_delete = $num_files - $num_to_keep;
+      // Delete from the start of the list (earliest).
+      foreach ($sorted as $id => $time) {
+        if (!$num_to_delete--) {
+          break;
+        }
+        $destination->delete_file($id);
+      }
+    }
+  }
+
+  /**
+   * Delete files keeping the specified number of hourly, daily, weekly and monthly backups.
+   */
+  public function smart_delete_backups($destination, $files, $keep_subhourly = 3600, $keep_hourly = 24, $keep_daily = 14, $keep_weekly = PHP_INT_MAX, $keep_monthly = PHP_INT_MAX) {
+    // Each period must be an exact multiple of the next smallest period.
+    $now = time();
+    $periods = array(
+      'subhourly' => array(
+        'delta' => 1,
+        'keep' => $keep_subhourly,
+        'last_time' => 0,
+        'files' => array(),
+      ),
+      'hourly' => array(
+        'delta' => 60 * 60,
+        'keep' => $keep_hourly,
+        'last_time' => 0,
+        'files' => array(),
+      ),
+      'daily' => array(
+        'delta' => 60 * 60 * 24,
+        'keep' => $keep_daily,
+        'last_time' => 0,
+        'files' => array(),
+      ),
+      'weekly' => array(
+        'delta' => 60 * 60 * 24 * 7,
+        'keep' => $keep_weekly,
+        'last_time' => 0,
+        'files' => array(),
+      ),
+      /*
+      'monthly' => array(
+        'delta' => 60*60*24*7*4,
+        'keep' => $keep_monthly,
+        'last_time' => 0,
+        'files' => array(),
+      ),
+      */
+    );
+
+    $keep_files = $filetimes = $times = $groups = $sorted = $saved = array();
+    foreach ($files as $id => $file) {
+      if ($file->is_recognized_type()) {
+        $time = $file->info('filetime');
+        $sorted[$id] = $time;
+      }
+    }
+    // Sort files, oldest first.
+    asort($sorted);
+
+    // Reset internal pointer and get the oldest file time.
+    $oldest_file_time = reset($sorted);
+    // Save the oldest file.
+    $keep_files[key($sorted)] = key($sorted);
+
+    foreach ($periods as $i => $period) {
+      $period_keep_files = array();
+      $time = $oldest_file_time;
+
+      // Set time from which we start saving files.
+      $period_start_time = $now - (($period['keep'] + 1) * $period['delta']);
+
+      // Increase time to within one period time span of the period start
+      // time. This keeps all the different period starts aligned.
+      if ($time < $period_start_time) {
+        $time += ((int) ceil(($period_start_time - $time) / $period['delta'])) * $period['delta'];
+      }
+
+      $file_id = $this->find_nearest_file($sorted, $time);
+
+      do {
+        $period_keep_files[$file_id] = $file_id;
+        $last_file_id = $file_id;
+        $time += $period['delta'];
+        $file_id = $this->find_nearest_file($sorted, $time);
+      } while ($time < $now);
+
+      $keep_files = array_merge($keep_files, $period_keep_files);
+    }
+
+    // Do the delete.
+    foreach ($files as $id => $file) {
+      if (!isset($keep_files[$id])) {
+        $destination->delete_file($file->file_id());
+      }
+    }
+  }
+
+  /**
+   *
+   */
+  protected function find_nearest_file($files, $time) {
+    $last_file_id = NULL;
+    $last_file_time = NULL;
+
+    foreach ($files as $id => $file_time) {
+      if ($file_time >= $time) {
+        if ($last_file_time == NULL) {
+          return $id;
+        }
+        if ($file_time == $time) {
+          return $id;
+        }
+        $time_to_prev = $time - $last_file_time;
+        $time_to_next = $file_time - $time;
+        if ($time_to_prev >= $time_to_next) {
+          return $id;
         }
-  
-        // If we are beyond our limit, remove as many as we need.
-        $num_files = count($files);
-  
-        if ($num_files > $num_to_keep) {
-          $num_to_delete = $num_files - $num_to_keep;
-          // Sort by date.
-          ksort($files);
-          // Delete from the start of the list (earliest).
-          for ($i = 0; $i < $num_to_delete; $i++) {
-            $file = array_shift($files);
-            $destination->delete_file($file->file_id());
-          }
+        else {
+          return $last_file_id;
         }
+
+        // Shouldn't hit this but you never know.
+        break;
       }
+      $last_file_id = $id;
+      $last_file_time = $file_time;
     }
   }
+
 }

+ 314 - 0
sites/all/modules/contrib/admin/backup_migrate/includes/sources.archivesource.inc

@@ -0,0 +1,314 @@
+<?php
+
+/**
+ * @file
+ */
+
+require_once dirname(__FILE__) . '/sources.filesource.inc';
+
+/**
+ * @file
+ * A destination type for saving locally to the server.
+ */
+
+/**
+ * A destination type for saving locally to the server.
+ *
+ * @ingroup backup_migrate_destinations
+ */
+class backup_migrate_files_destination_archivesource extends backup_migrate_destination_filesource {
+  public $supported_ops = array('source');
+
+  /**
+   *
+   */
+  public function type_name() {
+    return t("Site Archive Source");
+  }
+
+  /**
+   * Declares the current files directory as a backup source..
+   */
+  public function sources() {
+    $out = array();
+    $out['archive'] = backup_migrate_create_destination('archive', array(
+      'machine_name' => 'archive',
+      'location' => '.',
+      'name' => t('Entire Site (code, files & DB)'),
+      'show_in_list' => FALSE,
+    ));
+    return $out;
+  }
+
+  /**
+   * Returns a list of backup filetypes.
+   */
+  public function file_types() {
+    return array(
+      "sitearchive" => array(
+        "extension" => "sitearchive.tar",
+        "filemime" => "application/x-tar",
+        "backup" => TRUE,
+        "restore" => FALSE,
+      ),
+    );
+  }
+
+  /**
+   * Gets the form for the settings for this destination.
+   */
+  public function backup_settings_default() {
+    $out = parent::backup_settings_default();
+    $excludes = explode("\n", $out['exclude_filepaths']);
+    foreach ($excludes as $i => $exclude) {
+      $excludes[$i] = 'public://' . $exclude;
+    }
+    $excludes[] = 'private://backup_migrate';
+    $excludes[] = conf_path() . '/settings.php';
+    $excludes[] = file_directory_temp();
+
+    return array(
+      'exclude_filepaths' => implode("\n", $excludes),
+    );
+  }
+
+  /**
+   * Backup from this source.
+   */
+  public function _backup_to_file_php($file, $settings) {
+    if ($this->check_libs()) {
+      $base_dir = $this->get_realpath();
+
+      $excluded_paths = empty($settings->filters['exclude_filepaths']) ? '' : $settings->filters['exclude_filepaths'];
+      $exclude = $this->get_excluded_paths($settings);
+      $files = $this->get_files_to_backup($this->get_realpath(), $settings, $exclude);
+      if ($files) {
+        $manifest = $this->generate_manifest();
+        $db = $this->get_db();
+
+        $file->push_type('sitearchive');
+        $gz = new Archive_Tar($file->filepath(), FALSE);
+
+        $gz->addModify(array($manifest), $file->name . '/', dirname($manifest));
+        $gz->addModify($files, $file->name . '/docroot', $base_dir);
+        $gz->addModify($db, $file->name . '/', dirname($db));
+
+        unlink($manifest);
+        rmdir(dirname($manifest));
+        unlink($db);
+        rmdir(dirname($db));
+
+        return $file;
+      }
+      backup_migrate_backup_fail('No files available.', array(), $settings);
+      return FALSE;
+    }
+    return FALSE;
+  }
+
+  /**
+   * Backup from this source.
+   */
+  public function _backup_to_file_cli($file, $settings) {
+    if (!empty($settings->filters['use_cli']) && function_exists('backup_migrate_exec') && function_exists('escapeshellarg')) {
+      $excluded_paths = empty($settings->filters['exclude_filepaths']) ? '' : $settings->filters['exclude_filepaths'];
+      foreach ($this->get_excluded_paths($excluded_paths) as $path) {
+        $exclude[] = '--exclude=' . escapeshellarg($path);
+      }
+      $exclude = implode(' ', $exclude);
+
+      // Create a symlink in a temp directory so we can rename the file in the
+      // archive.
+      $temp = backup_migrate_temp_directory();
+
+      $manifest = $this->generate_manifest();
+      $db = $this->get_db();
+      rename($db, $temp . '/database.sql');
+      rename($manifest, $temp . '/MANIFEST.ini');
+
+      $file->push_type('sitearchive');
+      $link = $temp . '/docroot';
+      $input = realpath($this->get_location());
+      backup_migrate_exec("ln -s %input %link; tar --dereference -C %temp -rf %output $exclude .", array(
+        '%output' => $file->filepath(),
+        '%input' => $input,
+        '%temp' => $temp,
+        '%link' => $link,
+      ));
+
+      return $file;
+    }
+    return FALSE;
+  }
+
+  /**
+   * Generates a manifest file.
+   */
+  public function generate_manifest() {
+    $info = array(
+      'Global' => array(
+        'datestamp' => time(),
+        'formatversion' => '2011-07-02',
+        'generator' => 'Backup and Migrate (http://drupal.org/project/backup_migrate)',
+        'generatorversion' => BACKUP_MIGRATE_VERSION,
+      ),
+      'Site 0' => array(
+        'version' => VERSION,
+        'name' => variable_get('site_name', ''),
+        'docroot' => 'docroot',
+        'sitedir' => 'docroot/' . conf_path(),
+        'database-file-default' => 'database.sql',
+        'database-file-driver' => 'mysql',
+      ),
+    );
+    if ($private = variable_get('file_private_path', FALSE)) {
+      $info['Site 0']['files-private'] = 'docroot/' . $private;
+    }
+    $info['Site 0']['files-public'] = 'docroot/' . variable_get('file_public_path', FALSE);
+    $ini = $this->_array_to_ini($info);
+
+    $tmpdir = backup_migrate_temp_directory();
+    $filepath = $tmpdir . '/MANIFEST.ini';
+    file_put_contents($filepath, $ini);
+    return $filepath;
+  }
+
+  /**
+   * Gets a database dump to add to the archive.
+   */
+  public function get_db() {
+    require_once dirname(__FILE__) . '/destinations.inc';
+    require_once dirname(__FILE__) . '/files.inc';
+    require_once dirname(__FILE__) . '/filters.inc';
+    require_once dirname(__FILE__) . '/profiles.inc';
+
+    $file = new backup_file();
+    // Clone the default settings so we can make changes without them leaking
+    // out of this function.
+    $settings = clone _backup_migrate_profile_saved_default_profile();
+    $settings->source_id = 'db';
+    $settings->filters['compression'] = 'none';
+
+    // Execute the backup on the db with the default settings.
+    $file = backup_migrate_filters_backup($file, $settings);
+
+    // Generate a tmp file with the correct final title (because ArchiveTar
+    // doesn't seem to allow renaming).
+    $tmpdir = backup_migrate_temp_directory();
+    $filepath = $tmpdir . '/database.sql';
+    rename($file->filepath(), $filepath);
+
+    return $filepath;
+  }
+
+  /**
+   * Restores to this source.
+   */
+  public function _restore_from_file_php($file, &$settings) {
+    $success = FALSE;
+    if ($this->check_libs()) {
+      $from = $file->pop_type();
+      $temp = backup_migrate_temp_directory();
+
+      $tar = new Archive_Tar($from->filepath());
+      $tar->extractModify($temp, $file->name);
+
+      // Parse the manifest.
+      $manifest = $this->read_manifest($temp);
+
+      // Currently only the first site in the archive is supported.
+      $site = $manifest['Site 0'];
+
+      $docroot = $temp . '/' . $site['docroot'];
+      $sqlfile = $temp . '/' . $site['database-file-default'];
+      $filepath = NULL;
+      if (isset($site['files-private'])) {
+        $filepath = $temp . '/' . $site['files-private'];
+      }
+      elseif (isset($site['files-public'])) {
+        $filepath = $temp . '/' . $site['files-public'];
+      }
+
+      // Move the files from the temp directory.
+      if ($filepath && file_exists($filepath)) {
+        _backup_migrate_move_files($filepath, variable_get('file_public_path', conf_path() . '/files'));
+      }
+      else {
+        _backup_migrate_message('Files were not restored because the archive did not seem to contain a files directory or was in a format that Backup and Migrate couldn\'t read', array(), 'warning');
+      }
+
+      // Restore the sql db.
+      if ($sqlfile && file_exists($sqlfile)) {
+        $db_settings = clone $settings;
+        $db_settings->source_id = 'db';
+        $file = new backup_file(array('filepath' => $sqlfile));
+        $success = backup_migrate_filters_restore($file, $db_settings);
+      }
+      else {
+        _backup_migrate_message('The database was not restored because the archive did not seem to contain a database backup or was in a format that Backup and Migrate couldn\'t read', array(), 'warning');
+      }
+
+      if ($docroot) {
+        _backup_migrate_message('Backup and Migrate cannot restore the php code of the site for security reasons. You will have to copy the code to the server by hand if you wish to restore the full site.', array(), 'warning');
+      }
+
+      return $success && $file;
+    }
+    return FALSE;
+  }
+
+  /**
+   * Restores to this source.
+   */
+  public function _restore_from_file_cli($file, &$settings) {
+    // @todo implement the cli version of the restore.
+    return FALSE;
+  }
+
+  /**
+   * Generates a manifest file.
+   */
+  public function read_manifest($directory) {
+    // Assume some defaults if values ore the manifest is missing.
+    $defaults = array(
+      'docroot' => 'docroot',
+      'database-file-default' => 'database.sql',
+      'database-file-driver' => 'mysql',
+    );
+
+    $out = $this->_ini_to_array($directory . '/MANIFEST.ini');
+
+    // Set the defaults.
+    $out['Site 0'] = isset($out['Site 0']) ? $out['Site 0'] : array();
+    $out['Site 0'] += $defaults;
+
+    return $out;
+  }
+
+  /**
+   * Converts an associated array to an ini format string.
+   *
+   * Only allows 2 levels of depth to allow parse_ini_file to parse.
+   */
+  public function _array_to_ini($sections) {
+    $content = "";
+    foreach ($sections as $section => $data) {
+      $content .= '[' . $section . ']' . "\n";
+      foreach ($data as $key => $val) {
+        $content .= $key . " = \"" . $val . "\"\n";
+      }
+      $content .= "\n";
+    }
+    return $content;
+  }
+
+  /**
+   * Converts an associated array to an ini format string.
+   *
+   * Only allows 2 levels of depth to allow parse_ini_file to parse.
+   */
+  public function _ini_to_array($path) {
+    return parse_ini_file($path, TRUE);
+  }
+
+}

+ 330 - 0
sites/all/modules/contrib/admin/backup_migrate/includes/sources.db.inc

@@ -0,0 +1,330 @@
+<?php
+
+/**
+ * @file
+ * Functions to handle the direct to database destination.
+ */
+
+/**
+ * A destination type for saving to a database server.
+ *
+ * @ingroup backup_migrate_destinations
+ */
+class backup_migrate_source_db extends backup_migrate_source_remote {
+  public $supported_ops = array('configure', 'source');
+  public $db_target = 'default';
+  public $connection = NULL;
+
+  /**
+   *
+   */
+  public function type_name() {
+    return t("Database");
+  }
+
+  /**
+   * Save the info by importing it into the database.
+   */
+  public function save_file($file, $settings) {
+    require_once dirname(__FILE__) . '/files.inc';
+
+    // Set the source_id to the destination_id in the settings since for a
+    // restore, the source_id is the database that gets restored to.
+    $settings->set_source($this->get_id());
+
+    // Restore the file to the source database.
+    $file = backup_migrate_perform_restore($this->get_id(), $file, $settings);
+
+    return $file;
+  }
+
+  /**
+   * Destination configuration callback.
+   */
+  public function edit_form() {
+    $form = parent::edit_form();
+    $form['scheme']['#default_value'] = $this->default_scheme();
+    $form['scheme']['#access'] = FALSE;
+    $form['path']['#title'] = t('Database name');
+    $form['path']['#description'] = t('The name of the database. The database must exist, it will not be created for you.');
+    $form['user']['#description'] = t('Enter the name of a user who has write access to the database.');
+    return $form;
+  }
+
+  /**
+   * Validate the configuration form. Make sure the db info is valid.
+   */
+  public function edit_form_validate($form, &$form_state) {
+    if (!preg_match('/[a-zA-Z0-9_\$]+/', $form_state['values']['path'])) {
+      form_set_error('path', t('The database name is not valid.'));
+    }
+    parent::edit_form_validate($form, $form_state);
+  }
+
+  /**
+   * Get the default settings for this object.
+   *
+   * @return array
+   *   The default tables whose data can be ignored. These tables mostly
+   *   contain info which can be easily reproducted (such as cache or search
+   *   index) but also tables which can become quite bloated but are not
+   *   necessarily extremely important to back up or migrate during development
+   *   (such as access log and watchdog).
+   */
+  public function backup_settings_default() {
+    $all_tables = $this->_get_table_names();
+
+    // Basic modules that should be excluded.
+    $basic = array(
+      // Default core tables.
+      'accesslog',
+      'sessions',
+      'watchdog',
+      // Search module.
+      'search_dataset',
+      'search_index',
+      'search_keywords_log',
+      'search_total',
+      // Devel module.
+      'devel_queries',
+      'devel_times',
+    );
+
+    // Identify all cache tables.
+    $cache = array('cache');
+    foreach ($all_tables as $table_name) {
+      if (strpos($table_name, 'cache_') === 0) {
+        $cache[] = $table_name;
+      }
+    }
+
+    // Simpletest can create a lot of tables that do not need to be backed up,
+    // but all of them start with the string 'simpletest' so they can be easily
+    // excluded.
+    $simpletest = array();
+    foreach ($all_tables as $table_name) {
+      if (strpos($table_name, 'simpletest') === 0) {
+        $simpletest[] = $table_name;
+      }
+    }
+
+    return array(
+      'nodata_tables' => drupal_map_assoc(array_merge($basic, $cache, module_invoke_all('devel_caches'))),
+      'exclude_tables' => $simpletest,
+      'utils_lock_tables' => FALSE,
+    );
+  }
+
+  /**
+   * Get the form for the backup settings for this destination.
+   */
+  public function backup_settings_form($settings) {
+    $objects = $this->get_object_names();
+    $form['#description'] = t("You may omit specific tables, or specific table data from the backup file. Only omit data that you know you will not need such as cache data, or tables from other applications. Excluding tables can break your Drupal install, so <strong>do not change these settings unless you know what you're doing</strong>.");
+    $form['exclude_tables'] = array(
+      "#type" => "select",
+      "#multiple" => TRUE,
+      "#title" => t("Exclude the following tables altogether"),
+      "#options" => $objects,
+      "#default_value" => $settings['exclude_tables'],
+      "#description" => t("The selected tables will not be added to the backup file."),
+    );
+    $form['nodata_tables'] = array(
+      "#type" => "select",
+      "#multiple" => TRUE,
+      "#title" => t("Exclude the data from the following tables"),
+      "#options" => $objects,
+      "#default_value" => $settings['nodata_tables'],
+      "#description" => t("The selected tables will have their structure backed up but not their contents. This is useful for excluding cache data to reduce file size."),
+    );
+    $form['utils_lock_tables'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Lock tables during backup'),
+      '#default_value' => !empty($settings['utils_lock_tables']) ? $settings['utils_lock_tables'] : NULL,
+      '#description' => t('This can help reduce data corruption, but will make your site unresponsive.'),
+    );
+    return $form;
+  }
+
+  /**
+   * Backup from this source.
+   */
+  public function backup_to_file($file, $settings) {
+    $file->push_type($this->get_file_type_id());
+
+    // $this->lock_tables($settings);
+    // Switch to a different db if specified.
+    if (variable_get('backup_migrate_verbose')) {
+      _backup_migrate_message('Start peak memory usage: %mem', array('%mem' => backup_migrate_get_peak_memory_usage() . 'MB'), 'success');
+    }
+    $success = $this->_backup_db_to_file($file, $settings);
+    if (variable_get('backup_migrate_verbose')) {
+      _backup_migrate_message('Finish peak memory usage: %mem', array('%mem' => backup_migrate_get_peak_memory_usage() . 'MB'), 'success');
+    }
+
+    // $this->unlock_tables($settings);
+    return $success ? $file : FALSE;
+  }
+
+  /**
+   * Restore to this source.
+   */
+  public function restore_from_file($file, &$settings) {
+    $num = 0;
+    $type = $this->get_file_type_id();
+    // Open the file using the file wrapper. Check that the dump is of the right type (allow .sql for legacy reasons).
+    if ($file->type_id() !== $this->get_file_type_id() && $file->type_id() !== 'sql') {
+      _backup_migrate_message("Unable to restore from file %file because a %type file can't be restored to this database.", array("%file" => $file->filepath(), '%type' => $file->type_id()), 'error');
+    }
+    else {
+      backup_migrate_filters_invoke_all('pre_restore', $file, $settings);
+
+      // Restore the database.
+      $num = $this->_restore_db_from_file($file, $settings);
+      $settings->performed_action = $num ? t('%num SQL commands executed.', array('%num' => $num)) : '';
+
+      backup_migrate_filters_invoke_all('post_restore', $file, $settings, $num);
+    }
+    return $num;
+  }
+
+  /**
+   * Get the db connection for the specified db.
+   */
+  public function _get_db_connection() {
+    if (!$this->connection) {
+      $target = $key = '';
+      $parts = explode(':', $this->get_id());
+      // One of the predefined databases (set in settings.php)
+      if ($parts[0] == 'db') {
+        $key    = empty($parts[1]) ? 'default' : $parts[1];
+        $target = empty($parts[2]) ? 'default' : $parts[2];
+      }
+      // Another db url.
+      else {
+        // If the url is specified build it into a connection info array.
+        if (!empty($this->dest_url)) {
+          $info   = array(
+            'driver'    => empty($this->dest_url['scheme']) ? NULL : $this->dest_url['scheme'],
+            'host'      => empty($this->dest_url['host']) ? NULL : $this->dest_url['host'],
+            'port'      => empty($this->dest_url['port']) ? NULL : $this->dest_url['port'],
+            'username'  => empty($this->dest_url['user']) ? NULL : $this->dest_url['user'],
+            'password'  => empty($this->dest_url['pass']) ? NULL : $this->dest_url['pass'],
+            'database'  => empty($this->dest_url['path']) ? NULL : $this->dest_url['path'],
+          );
+          $key    = uniqid('backup_migrate_tmp_');
+          $target = 'default';
+          Database::addConnectionInfo($key, $target, $info);
+        }
+        // No database selected. Assume the default.
+        else {
+          $key = $target = 'default';
+        }
+      }
+      if ($target && $key) {
+        $this->connection = Database::getConnection($target, $key);
+      }
+    }
+    return $this->connection;
+  }
+
+  /**
+   * Backup the databases to a file.
+   */
+  public function _backup_db_to_file($file, $settings) {
+    // Must be overridden.
+  }
+
+  /**
+   * Backup the databases to a file.
+   */
+  public function _restore_db_from_file($file, $settings) {
+    // Must be overridden.
+  }
+
+  /**
+   * Get a list of objects in the database.
+   */
+  public function get_object_names() {
+    // Must be overridden.
+    $out = $this->_get_table_names();
+    if (method_exists($this, '_get_view_names')) {
+      $out += $this->_get_view_names();
+    }
+    return $out;
+  }
+
+  /**
+   * Get a list of tables in the database.
+   */
+  public function get_table_names() {
+    // Must be overridden.
+    $out = $this->_get_table_names();
+    return $out;
+  }
+
+  /**
+   * Get a list of tables in the database.
+   */
+  public function _get_table_names() {
+    // Must be overridden.
+    return array();
+  }
+
+  /**
+   * Lock the database in anticipation of a backup.
+   */
+  public function lock_tables($settings) {
+    if ($settings->filters['utils_lock_tables']) {
+      $tables = array();
+      foreach ($this->get_table_names() as $table) {
+        // There's no need to lock excluded or structure only tables because it doesn't matter if they change.
+        if (empty($settings->filters['exclude_tables']) || !in_array($table, (array) $settings->filters['exclude_tables'])) {
+          $tables[] = $table;
+        }
+      }
+      $this->_lock_tables($tables);
+    }
+  }
+
+  /**
+   * Lock the list of given tables in the database.
+   */
+  public function _lock_tables($tables) {
+    // Must be overridden.
+  }
+
+  /**
+   * Unlock any tables that have been locked.
+   */
+  public function unlock_tables($settings) {
+    if ($settings->filters['utils_lock_tables']) {
+      $this->_unlock_tables();
+    }
+  }
+
+  /**
+   * Unlock the list of given tables in the database.
+   */
+  public function _unlock_tables($tables) {
+    // Must be overridden.
+  }
+
+  /**
+   * Get the file type for to backup this destination to.
+   */
+  public function get_file_type_id() {
+    return 'sql';
+  }
+
+  /**
+   * Get the version info for the given DB.
+   */
+  public function _db_info() {
+    return array(
+      'type' => FALSE,
+      'version' => t('Unknown'),
+    );
+  }
+
+}

+ 593 - 0
sites/all/modules/contrib/admin/backup_migrate/includes/sources.db.mysql.inc

@@ -0,0 +1,593 @@
+<?php
+
+/**
+ * @file
+ */
+
+require_once dirname(__FILE__) . '/sources.db.inc';
+
+/**
+ * @file
+ * Functions to handle the direct to database source.
+ */
+
+/**
+ * A source type for backing up from database server.
+ *
+ * @ingroup backup_migrate_destinations
+ */
+class backup_migrate_source_db_mysql extends backup_migrate_source_db {
+
+  /**
+   * The table's data keyed by table name.
+   *
+   * @var array
+   */
+  protected static $tableData = array();
+
+  /**
+   * The tables keyed by name.
+   *
+   * @var array
+   */
+  protected static $tableNames = array();
+
+  /**
+   * The views keyed by name.
+   *
+   * @var array
+   */
+  protected static $viewNames = array();
+
+  /**
+   *
+   */
+  public function type_name() {
+    return t("MySQL Database");
+  }
+
+  /**
+   * Return a list of backup filetypes.
+   */
+  public function file_types() {
+    return array(
+      "sql" => array(
+        "extension" => "sql",
+        "filemime" => "text/x-sql",
+        "backup" => TRUE,
+        "restore" => TRUE,
+      ),
+      "mysql" => array(
+        "extension" => "mysql",
+        "filemime" => "text/x-sql",
+        "backup" => TRUE,
+        "restore" => TRUE,
+      ),
+    );
+  }
+
+  /**
+   * Return the scheme for this db type.
+   */
+  public function default_scheme() {
+    return 'mysql';
+  }
+
+  /**
+   * Declare any mysql databases defined in the settings.php file as a possible source.
+   */
+  public function sources() {
+    $out = array();
+    global $databases;
+    foreach ((array) $databases as $db_key => $target) {
+      foreach ((array) $target as $tgt_key => $info) {
+        // Only mysql/mysqli supported by this source.
+        $key = $db_key . ':' . $tgt_key;
+        if ($info['driver'] === 'mysql') {
+          // Compile the database connection string.
+          $url = 'mysql://';
+          $url .= urlencode($info['username']) . ':' . urlencode($info['password']);
+          $url .= '@';
+          $url .= urlencode($info['host']);
+          if (!empty($info['port'])) {
+            $url .= ':' . $info['port'];
+          }
+          $url .= '/' . urlencode($info['database']);
+
+          if ($source = backup_migrate_create_destination('mysql', array('url' => $url))) {
+            // Treat the default database differently because it is probably
+            // the only one available.
+            if ($key == 'default:default') {
+              $source->set_id('db');
+              $source->set_name(t('Default Database'));
+              // Dissalow backing up to the default database because that's confusing and potentially dangerous.
+              $source->remove_op('scheduled backup');
+              $source->remove_op('manual backup');
+            }
+            else {
+              $source->set_id('db:' . $key);
+              $source->set_name($key . ": " . $source->get_display_location());
+            }
+            $out[$source->get_id()] = $source;
+          }
+        }
+      }
+    }
+    return $out;
+  }
+
+  /**
+   * Get the file type for to backup this source to.
+   */
+  public function get_file_type_id() {
+    return 'mysql';
+  }
+
+  /**
+   * Backup the databases to a file.
+   *
+   *  Returns a list of sql commands, one command per line.
+   *  That makes it easier to import without loading the whole file into memory.
+   *  The files are a little harder to read, but human-readability is not a priority.
+   */
+  public function _backup_db_to_file($file, $settings) {
+    if (!empty($settings->filters['use_cli']) && $this->_backup_db_to_file_mysqldump($file, $settings)) {
+      return TRUE;
+    }
+
+    $lines = 0;
+    $exclude = !empty($settings->filters['exclude_tables']) ? $settings->filters['exclude_tables'] : array();
+    $nodata = !empty($settings->filters['nodata_tables']) ? $settings->filters['nodata_tables'] : array();
+    if ($file->open(TRUE)) {
+      $file->write($this->_get_sql_file_header());
+      $alltables = $this->_get_tables();
+      $allviews = $this->_get_views();
+
+      foreach ($alltables as $table) {
+        if (_backup_migrate_check_timeout()) {
+          return FALSE;
+        }
+        if ($table['name'] && !isset($exclude[$table['name']])) {
+          $file->write($this->_get_table_structure_sql($table));
+          $lines++;
+          if (!in_array($table['name'], $nodata)) {
+            $lines += $this->_dump_table_data_sql_to_file($file, $table);
+          }
+        }
+      }
+      foreach ($allviews as $view) {
+        if (_backup_migrate_check_timeout()) {
+          return FALSE;
+        }
+        if ($view['name'] && !isset($exclude[$view['name']])) {
+          $file->write($this->_get_view_create_sql($view));
+        }
+      }
+      $file->write($this->_get_sql_file_footer());
+      $file->close();
+      return $lines;
+    }
+    else {
+      return FALSE;
+    }
+  }
+
+  /**
+   * Backup the databases to a file using the mysqldump command.
+   */
+  public function _backup_db_to_file_mysqldump($file, $settings) {
+    $success = FALSE;
+    $nodata_tables = array();
+    $alltables = $this->_get_tables();
+
+    $command = 'mysqldump --result-file=%file --opt -Q --host=%host --port=%port --user=%user --password=%pass %db';
+    $args = array(
+      '%file' => $file->filepath(),
+      '%host' => $this->dest_url['host'],
+      '%port' => isset($this->dest_url['port']) ? $this->dest_url['port'] : '3306',
+      '%user' => $this->dest_url['user'],
+      '%pass' => $this->dest_url['pass'],
+      '%db' => $this->dest_url['path'],
+    );
+
+    // Ignore the excluded and no-data tables.
+    $db = $this->dest_url['path'];
+    if (!empty($settings->filters['exclude_tables'])) {
+      foreach ((array) $settings->filters['exclude_tables'] as $table) {
+        if (isset($alltables[$table])) {
+          $command .= ' --ignore-table=' . $db . '.' . $table;
+        }
+      }
+    }
+    if (!empty($settings->filters['nodata_tables'])) {
+      foreach ((array) $settings->filters['nodata_tables'] as $table) {
+        if (isset($alltables[$table])) {
+          $nodata_tables[] = $table;
+          $command .= ' --ignore-table=' . $db . '.' . $table;
+        }
+      }
+    }
+    $success = backup_migrate_exec($command, $args);
+
+    // Get the nodata tables.
+    if ($success && !empty($nodata_tables)) {
+      $tables = implode(' ', array_unique($nodata_tables));
+      $command = "mysqldump --no-data --opt -Q --host=%host --port=%port --user=%user --password=%pass %db $tables >> %file";
+      $success = backup_migrate_exec($command, $args);
+    }
+    return $success;
+  }
+
+  /**
+   * Backup the databases to a file.
+   */
+  public function _restore_db_from_file($file, $settings) {
+    $num = 0;
+
+    if ($file->open() && $conn = $this->_get_db_connection()) {
+      // Optionally drop all existing tables.
+      if (!empty($settings->filters['utils_drop_all_tables'])) {
+        $all_tables = $this->_get_tables();
+        $table_names = array_map('backup_migrate_array_name_value', $all_tables);
+        $table_list = join(', ', $table_names);
+        $stmt = $conn->prepare("DROP TABLE IF EXISTS $table_list;\n");
+        $stmt->execute();
+      }
+
+      // Read one line at a time and run the query.
+      while ($line = $this->_read_sql_command_from_file($file)) {
+        if (_backup_migrate_check_timeout()) {
+          return FALSE;
+        }
+        if ($line) {
+          // Prepeare and exexute the statement instead of the api function to avoid substitution of '{' etc.
+          $stmt = $conn->prepare($line);
+          $stmt->execute();
+          $num++;
+        }
+      }
+      // Close the file with fclose/gzclose.
+      $file->close();
+    }
+    else {
+      drupal_set_message(t("Unable to open file %file to restore database", array("%file" => $file->filepath())), 'error');
+      $num = FALSE;
+    }
+    return $num;
+  }
+
+  /**
+   * Read a multiline sql command from a file.
+   *
+   * Supports the formatting created by mysqldump, but won't handle multiline comments.
+   */
+  public function _read_sql_command_from_file($file) {
+    $out = '';
+    while ($line = $file->read()) {
+      $first2 = substr($line, 0, 2);
+      $first3 = substr($line, 0, 3);
+
+      // Ignore single line comments. This function doesn't support multiline comments or inline comments.
+      if ($first2 != '--' && ($first2 != '/*' || $first3 == '/*!')) {
+        $out .= ' ' . trim($line);
+        // If a line ends in ; or */ it is a sql command.
+        if (substr($out, strlen($out) - 1, 1) == ';') {
+          return trim($out);
+        }
+      }
+    }
+    return trim($out);
+  }
+
+  /**
+   * Get a list of tables in the database.
+   */
+  public function _get_table_names() {
+    if (empty(static::$tableNames)) {
+      static::$tableNames = $this->query("SHOW FULL TABLES WHERE Table_Type = 'BASE TABLE'")
+        ->fetchAllKeyed(0, 0);
+    }
+
+    return static::$tableNames;
+  }
+
+  /**
+   * Get a list of views in the database.
+   */
+  public function _get_view_names() {
+    if (empty(static::$viewNames)) {
+      static::$viewNames = $this->query("SHOW FULL TABLES WHERE Table_Type = 'VIEW'")
+        ->fetchAllKeyed(0, 0);
+    }
+
+    return static::$viewNames;
+  }
+
+  /**
+   * Lock the list of given tables in the database.
+   */
+  public function _lock_tables($tables) {
+    if ($tables) {
+      $tables_escaped = array();
+      foreach ($tables as $table) {
+        $tables_escaped[] = '`' . db_escape_table($table) . '`  WRITE';
+      }
+      $this->query('LOCK TABLES ' . implode(', ', $tables_escaped));
+    }
+  }
+
+  /**
+   * Unlock all tables in the database.
+   */
+  public function _unlock_tables($settings) {
+    $this->query('UNLOCK TABLES');
+  }
+
+  /**
+   * Get a list of table and view data in the db.
+   */
+  protected function get_table_data() {
+    if (empty(static::$tableData)) {
+      $tables = $this->query('SHOW TABLE STATUS')->fetchAll(PDO::FETCH_ASSOC);
+
+      foreach ($tables as $table) {
+        // Lowercase the keys because between Drupal 7.12 and 7.13/14 the
+        // default query behavior was changed.
+        // See: http://drupal.org/node/1171866
+        $table = array_change_key_case($table);
+        static::$tableData[$table['name']] = $table;
+      }
+    }
+
+    return static::$tableData;
+  }
+
+  /**
+   * Get a list of tables in the db.
+   */
+  public function _get_tables() {
+    $out = array();
+    foreach ($this->get_table_data() as $table) {
+      if (!empty($table['engine'])) {
+        $out[$table['name']] = $table;
+      }
+    }
+
+    return $out;
+  }
+
+  /**
+   * Get a list of views in the db.
+   */
+  public function _get_views() {
+    $out = array();
+    foreach ($this->get_table_data() as $table) {
+      if (empty($table['engine'])) {
+        $out[$table['name']] = $table;
+      }
+    }
+    return $out;
+  }
+
+  /**
+   * Get the sql for the structure of the given view.
+   */
+  public function _get_view_create_sql($view) {
+    $out = "";
+    // Switch SQL mode to get rid of "CREATE ALGORITHM..." what requires more permissions + troubles with the DEFINER user.
+    $sql_mode = $this->query("SELECT @@SESSION.sql_mode")->fetchField();
+    $this->query("SET sql_mode = 'ANSI'");
+    $result = $this->query("SHOW CREATE VIEW `" . $view['name'] . "`", array(), array('fetch' => PDO::FETCH_ASSOC));
+    $this->query("SET SQL_mode = :mode", array(':mode' => $sql_mode));
+    foreach ($result as $create) {
+      // Lowercase the keys because between Drupal 7.12 and 7.13/14 the default query behavior was changed.
+      // See: http://drupal.org/node/1171866
+      $create = array_change_key_case($create);
+      $out .= "DROP VIEW IF EXISTS `" . $view['name'] . "`;\n";
+      $out .= "SET sql_mode = 'ANSI';\n";
+      $out .= strtr($create['create view'], "\n", " ") . ";\n";
+      $out .= "SET sql_mode = '$sql_mode';\n";
+    }
+    return $out;
+  }
+
+  /**
+   * Get the sql for the structure of the given table.
+   */
+  public function _get_table_structure_sql($table) {
+    $out = "";
+    $result = $this->query("SHOW CREATE TABLE `" . $table['name'] . "`", array(), array('fetch' => PDO::FETCH_ASSOC));
+    foreach ($result as $create) {
+      // Lowercase the keys because between Drupal 7.12 and 7.13/14 the default query behavior was changed.
+      // See: http://drupal.org/node/1171866
+      $create = array_change_key_case($create);
+      $out .= "DROP TABLE IF EXISTS `" . $table['name'] . "`;\n";
+      // Remove newlines and convert " to ` because PDO seems to convert those for some reason.
+      $out .= strtr($create['create table'], array("\n" => ' ', '"' => '`'));
+      if ($table['auto_increment']) {
+        $out .= " AUTO_INCREMENT=" . $table['auto_increment'];
+      }
+      $out .= ";\n";
+    }
+    return $out;
+  }
+
+  /**
+   * Get the sql to insert the data for a given table.
+   */
+  public function _dump_table_data_sql_to_file($file, $table) {
+    $rows_per_query = variable_get('backup_migrate_data_rows_per_query', BACKUP_MIGRATE_DATA_ROWS_PER_QUERY);
+    $rows_per_line = variable_get('backup_migrate_data_rows_per_line', BACKUP_MIGRATE_DATA_ROWS_PER_LINE);
+    $bytes_per_line = variable_get('backup_migrate_data_bytes_per_line', BACKUP_MIGRATE_DATA_BYTES_PER_LINE);
+
+    if (variable_get('backup_migrate_verbose')) {
+      _backup_migrate_message('Table: %table', array('%table' => $table['name']), 'success');
+    }
+
+    // Escape backslashes, PHP code, special chars.
+    $search = array('\\', "'", "\x00", "\x0a", "\x0d", "\x1a");
+    $replace = array('\\\\', "''", '\0', '\n', '\r', '\Z');
+
+    $lines = 0;
+    $from = 0;
+    $args = array('fetch' => PDO::FETCH_ASSOC);
+    while ($data = $this->query("SELECT * FROM `" . $table['name'] . "`", array(), $args, $from, $rows_per_query)) {
+      if ($data->rowCount() == 0) {
+        break;
+      }
+
+      $rows = $bytes = 0;
+
+      $line = array();
+      foreach ($data as $row) {
+        $from++;
+
+        // DB Escape the values.
+        $items = array();
+        foreach ($row as $key => $value) {
+          $items[] = is_null($value) ? "null" : "'" . str_replace($search, $replace, $value) . "'";
+        }
+
+        // If there is a row to be added.
+        if ($items) {
+          // Start a new line if we need to.
+          if ($rows == 0) {
+            $file->write("INSERT INTO `" . $table['name'] . "` VALUES ");
+            $bytes = $rows = 0;
+          }
+          // Otherwise add a comma to end the previous entry.
+          else {
+            $file->write(",");
+          }
+
+          // Write the data itself.
+          $sql = implode(',', $items);
+          $file->write('(' . $sql . ')');
+          $bytes += strlen($sql);
+          $rows++;
+
+          // Finish the last line if we've added enough items.
+          if ($rows >= $rows_per_line || $bytes >= $bytes_per_line) {
+            $file->write(";\n");
+            $lines++;
+            $bytes = $rows = 0;
+          }
+        }
+      }
+
+      // Finish any unfinished insert statements.
+      if ($rows > 0) {
+        $file->write(";\n");
+        $lines++;
+      }
+    }
+
+    if (variable_get('backup_migrate_verbose')) {
+      _backup_migrate_message('Peak memory usage: %mem', array('%mem' => backup_migrate_get_peak_memory_usage() . 'MB'), 'success');
+    }
+
+    return $lines;
+  }
+
+  /**
+   * Get the db connection for the specified db.
+   */
+  public function _get_db_connection() {
+    if (!$this->connection) {
+      $this->connection = parent::_get_db_connection();
+      // Set the sql mode because the default is ANSI,TRADITIONAL which is not aware of collation or storage engine.
+      $this->connection->exec("SET sql_mode=''");
+    }
+    return $this->connection;
+  }
+
+  /**
+   * Run a query on this source's database using Drupal's MySQL engine.
+   *
+   * @param string $query
+   *   The query string.
+   * @param array $args
+   *   Arguments for the query.
+   * @param array $options
+   *   Options to pass to the query.
+   * @param int|null $from
+   *   The starting point for the query; when passed will perform a queryRange()
+   *   method instead of a regular query().
+   * @param int|null $count
+   *   The number of records to obtain from this query. Will be ignored if the
+   *   $from argument is empty.
+   *
+   * @see DatabaseConnection_mysql::query()
+   * @see DatabaseConnection_mysql::queryRange()
+   */
+  public function query($query, array $args = array(), array $options = array(), $from = NULL, $count = NULL) {
+    if ($conn = $this->_get_db_connection()) {
+      // If no $from is passed in, just do a basic query.
+      if (is_null($from)) {
+        return $conn->query($query, $args, $options);
+      }
+      // The $from variable was passed in, so do a ranged query.
+      else {
+        return $conn->queryRange($query, $from, $count, $args, $options);
+      }
+    }
+  }
+
+  /**
+   * The header for the top of the sql dump file. These commands set the connection
+   *  character encoding to help prevent encoding conversion issues.
+   */
+  public function _get_sql_file_header() {
+    $info = $this->_db_info();
+
+    return "-- Backup and Migrate (Drupal) MySQL Dump
+-- Backup and Migrate Version: " . BACKUP_MIGRATE_VERSION . "
+-- http://drupal.org/project/backup_migrate
+-- Drupal Version: " . VERSION . "
+-- http://drupal.org/
+--
+-- Host: " . url('', array('absolute' => TRUE)) . "
+-- Site Name: " . url('', array('absolute' => TRUE)) . "
+-- Generation Time: " . format_date(time(), 'custom', 'r') . "
+-- MySQL Version: " . $info['version'] . "
+
+/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
+/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
+/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
+/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE=NO_AUTO_VALUE_ON_ZERO */;
+
+SET SQL_MODE=\"NO_AUTO_VALUE_ON_ZERO\";
+SET NAMES utf8mb4;
+
+";
+  }
+
+  /**
+   * The footer of the sql dump file.
+   */
+  public function _get_sql_file_footer() {
+    return "
+
+/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
+/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
+/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
+/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
+/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
+/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
+";
+  }
+
+  /**
+   * Get the version info for the given DB.
+   */
+  public function _db_info() {
+    $db = $this->_get_db_connection();
+    return array(
+      'type' => 'mysql',
+      'version' => $db ? $db->version() : t('Unknown'),
+    );
+  }
+
+}

+ 335 - 0
sites/all/modules/contrib/admin/backup_migrate/includes/sources.filesource.inc

@@ -0,0 +1,335 @@
+<?php
+
+/**
+ * @file
+ * A destination type for saving locally to the server.
+ */
+
+/**
+ * A destination type for saving locally to the server.
+ *
+ * @ingroup backup_migrate_destinations
+ */
+class backup_migrate_destination_filesource extends backup_migrate_source {
+  public $supported_ops = array('restore', 'configure', 'delete', 'source');
+
+  /**
+   *
+   */
+  public function type_name() {
+    return t("Files Directory");
+  }
+
+  /**
+   * Declares the current files directory as a backup source..
+   */
+  public function sources() {
+    $out = array();
+    $out['files'] = backup_migrate_create_destination('filesource', array(
+      'machine_name' => 'files',
+      'location' => 'public://',
+      'name' => t('Public Files Directory'),
+      'show_in_list' => FALSE,
+    ));
+    if (variable_get('file_private_path', FALSE)) {
+      $out['files_private'] = backup_migrate_create_destination('filesource', array(
+        'machine_name' => 'files',
+        'location' => 'private://',
+        'name' => t('Private Files Directory'),
+        'show_in_list' => FALSE,
+      ));
+    }
+    return $out;
+  }
+
+  /**
+   * Gets the form for the settings for the files destination.
+   */
+  public function edit_form() {
+    $form = parent::edit_form();
+    $form['location'] = array(
+      "#type" => "textfield",
+      "#title" => t("Directory path"),
+      "#default_value" => $this->get_location(),
+      "#required" => TRUE,
+      "#description" => t('Enter the path to the directory to back up. Use a relative path to pick a path relative to your Drupal root directory. The web server must be able to read from this path.'),
+    );
+    return $form;
+  }
+
+  /**
+   * Returns a list of backup filetypes.
+   */
+  public function file_types() {
+    return array(
+      "tar" => array(
+        "extension" => "tar",
+        "filemime" => "application/x-tar",
+        "backup" => TRUE,
+        "restore" => TRUE,
+      ),
+    );
+  }
+
+  /**
+   * Gets the form for the settings for this destination.
+   *
+   * Return the default directories whose data can be ignored. These directories
+   * contain info which can be easily reproducted. Also exclude the backup and
+   * migrate folder to prevent exponential bloat.
+   */
+  public function backup_settings_default() {
+    return array(
+      'exclude_filepaths' => "backup_migrate\nstyles\ncss\njs\nctools\nless\nlanguages\nadvagg_css\nadvagg_js",
+    );
+  }
+
+  /**
+   * Get the form for the backup settings for this destination.
+   */
+  public function backup_settings_form($settings) {
+    $form['exclude_filepaths'] = array(
+      "#type" => "textarea",
+      "#multiple" => TRUE,
+      "#title" => t("Exclude the following files or directories"),
+      "#default_value" => isset($settings['exclude_filepaths']) ? $settings['exclude_filepaths'] : '',
+      "#description" => t("A list of files or directories to be excluded from backups. Add one path per line relative to the directory being backed up."),
+    );
+    return $form;
+  }
+
+  /**
+   * Backup from this source.
+   */
+  public function backup_to_file($file, $settings) {
+    if ($out = $this->_backup_to_file_cli($file, $settings)) {
+      return $out;
+    }
+    else {
+      return $this->_backup_to_file_php($file, $settings);
+    }
+  }
+
+  /**
+   * Backup from this source.
+   */
+  public function _backup_to_file_php($file, $settings) {
+    if ($this->check_libs()) {
+      $excluded = $this->get_excluded_paths($settings);
+      $files = $this->get_files_to_backup($this->get_realpath(), $settings, $excluded);
+      if ($files) {
+        $file->push_type('tar');
+        $gz = new Archive_Tar($file->filepath(), FALSE);
+        $gz->addModify($files, '', $this->get_realpath());
+        return $file;
+      }
+      backup_migrate_backup_fail('No files available.', array(), $settings);
+      return FALSE;
+    }
+    return FALSE;
+  }
+
+  /**
+   * Backup from this source.
+   */
+  public function _backup_to_file_cli($file, $settings) {
+    if (!empty($settings->filters['use_cli']) && function_exists('backup_migrate_exec') && function_exists('escapeshellarg')) {
+      $excluded = $this->get_excluded_paths($settings);
+      $exclude = array();
+      foreach ($excluded as $path) {
+        $exclude[] = '--exclude=' . escapeshellarg($path);
+      }
+      $exclude = implode(' ', $exclude);
+
+      // Create a symlink in a temp directory so we can rename the file in the
+      // archive.
+      $temp = backup_migrate_temp_directory();
+
+      $file->push_type('tar');
+      backup_migrate_exec("tar --dereference -C %input -rf %output $exclude .", array(
+        '%output' => $file->filepath(),
+        '%input' => $this->get_realpath(),
+        '%temp' => $temp,
+      ));
+      return $file;
+    }
+    return FALSE;
+  }
+
+  /**
+   * Restore to this source.
+   */
+  public function restore_from_file($file, &$settings) {
+    if ($out = $this->_restore_from_file_cli($file, $settings)) {
+      return $out;
+    }
+    else {
+      return $this->_restore_from_file_php($file, $settings);
+    }
+  }
+
+  /**
+   * Restore to this source.
+   */
+  public function _restore_from_file_php($file, &$settings) {
+    if ($this->check_libs()) {
+      $from = $file->pop_type();
+      $temp = backup_migrate_temp_directory();
+
+      $tar = new Archive_Tar($from->filepath());
+      $tar->extractModify($temp, $file->name);
+
+      // Older B&M Files format included a base 'files' directory.
+      if (file_exists($temp . '/files')) {
+        $temp = $temp . '/files';
+      }
+      if (file_exists($temp . '/' . $file->name . '/files')) {
+        $temp = $temp . '/files';
+      }
+
+      // Move the files from the temp directory.
+      _backup_migrate_move_files($temp, $this->get_realpath());
+
+      return $file;
+    }
+    return FALSE;
+  }
+
+  /**
+   * Restore to this source.
+   */
+  public function _restore_from_file_cli($file, &$settings) {
+    if (!empty($settings->filters['use_cli']) && function_exists('backup_migrate_exec')) {
+      $temp = backup_migrate_temp_directory();
+      backup_migrate_exec("tar -C %temp -xf %input", array('%input' => $file->filepath(), '%temp' => $temp));
+
+      // Older B&M Files format included a base 'files' directory.
+      if (file_exists($temp . '/files')) {
+        $temp = $temp . '/files';
+      }
+      if (file_exists($temp . '/' . $file->name . '/files')) {
+        $temp = $temp . '/files';
+      }
+
+      // Move the files from the temp directory.
+      backup_migrate_exec("mv -rf %temp/* %output", array('%output' => $this->get_realpath(), '%temp' => $temp));
+      return $file;
+    }
+    return FALSE;
+  }
+
+  /**
+   * Gets a list of files to backup from the given set if dirs.
+   *
+   * Exclude any that match the array $exclude.
+   */
+  public function get_files_to_backup($dir, $settings, $exclude = array()) {
+    $out = $errors = array();
+
+    if (!file_exists($dir)) {
+      backup_migrate_backup_fail('Directory %dir does not exist.', array('%dir' => $dir), $settings);
+      return FALSE;
+    }
+    if ($handle = @opendir($dir)) {
+      while (($file = readdir($handle)) !== FALSE) {
+        if ($file != '.' && $file != '..' && !in_array($file, $exclude)) {
+          $real = realpath($dir . '/' . $file);
+          // If the path is not excluded.
+          if (!in_array($real, $exclude)) {
+            if (is_dir($real)) {
+              $subdir = $this->get_files_to_backup($real, $settings, $exclude);
+              // If there was an error reading the subdirectory then abort the
+              // backup.
+              if ($subdir === FALSE) {
+                closedir($handle);
+                return FALSE;
+              }
+              // If the directory is empty, add an empty directory.
+              if (count($subdir) == 0) {
+                $out[] = $real;
+              }
+              $out = array_merge($out, $subdir);
+            }
+            else {
+              if (is_readable($real)) {
+                $out[] = $real;
+              }
+              else {
+                $errors[] = $dir . '/' . $file;
+              }
+            }
+          }
+        }
+      }
+      closedir($handle);
+    }
+    else {
+      backup_migrate_backup_fail('Could not open directory %dir', array('%dir' => $dir), $settings);
+      return FALSE;
+    }
+
+    // Alert the user to any errors there might have been.
+    if ($errors) {
+      if (count($errors < 5)) {
+        $filesmsg = t('The following files: !files', array('!files' => theme('item_list', array('items' => $errors))));
+      }
+      else {
+        $filesmsg = t('!count files', array('!count' => count($errors)));
+      }
+
+      if (empty($settings->filters['ignore_errors'])) {
+        backup_migrate_backup_fail('The backup could not be completed because !files could not be read. If you want to skip unreadable files use the \'Ignore Errors\' setting under \'Advanced Options\' in \'Advanced Backup\' or in your schedule settings profile.', array('!files' => $filesmsg), 'error');
+        $out = FALSE;
+      }
+      else {
+        backup_migrate_backup_fail('!files could not be read and were skipped', array('!files' => $filesmsg), 'error');
+      }
+    }
+
+    return $out;
+  }
+
+  /**
+   * Breaks the excluded paths string into a usable list of paths.
+   */
+  public function get_excluded_paths($settings) {
+    $base_dir = $this->get_realpath() . '/';
+    $paths = empty($settings->filters['exclude_filepaths']) ? '' : $settings->filters['exclude_filepaths'];
+    $out = explode("\n", $paths);
+    foreach ($out as $key => $val) {
+      $path = trim($val, "/ \t\r\n");
+      // If the path specified is a stream url or absolute path add the
+      // normalized version.
+      if ($real = drupal_realpath($path)) {
+        $out[$key] = $real;
+      }
+      // If the path is a relative path add it.
+      elseif ($real = drupal_realpath($base_dir . $path)) {
+        $out[$key] = $real;
+      }
+      // Otherwise add it as is even though it probably won't match any files.
+      else {
+        $out[$key] = $path;
+      }
+    }
+    return $out;
+  }
+
+  /**
+   * Checks that the required libraries are installed.
+   */
+  public function check_libs() {
+    $result = TRUE;
+    // Drupal 7 has Archive_Tar built in so there should be no need to include
+    // anything here.
+    return $result;
+  }
+
+  /**
+   * Get the file location.
+   */
+  public function get_realpath() {
+    return drupal_realpath($this->get_location());
+  }
+
+}

+ 274 - 0
sites/all/modules/contrib/admin/backup_migrate/includes/sources.inc

@@ -0,0 +1,274 @@
+<?php
+
+/**
+ * @file
+ * All of the source handling code needed for Backup and Migrate.
+ */
+
+require_once dirname(__FILE__) . '/crud.inc';
+require_once dirname(__FILE__) . '/destinations.inc';
+require_once dirname(__FILE__) . '/locations.inc';
+
+/**
+ * Get all the available backup sources.
+ */
+function backup_migrate_get_sources() {
+  return backup_migrate_crud_get_items('source');
+}
+
+/**
+ * Get the available source types.
+ */
+function backup_migrate_get_source_subtypes() {
+  return backup_migrate_crud_subtypes('source');
+}
+
+/**
+ * Get the destination of the given id.
+ */
+function backup_migrate_get_source($id) {
+  $sources = backup_migrate_get_sources();
+  return empty($sources[$id]) ? NULL : $sources[$id];
+}
+
+/**
+ * Create a source object of the given type with the given params.
+ */
+function backup_migrate_create_source($subtype, $params = array()) {
+  $params['subtype'] = $subtype;
+  return backup_migrate_crud_create_item('source', $params);
+}
+
+/**
+ * Implements hook_backup_migrate_source_subtypes().
+ *
+ * Get the built in Backup and Migrate source types.
+ */
+function backup_migrate_backup_migrate_source_subtypes() {
+  $out = array();
+  $out += array(
+    'db' => array(
+      'type_name' => t('Database'),
+      'description' => t('Import the backup directly into another database. Database sources can also be used as a source to backup from.'),
+      'file' => drupal_get_path('module', 'backup_migrate') . '/includes/sources.db.inc',
+      'class' => 'backup_migrate_source_db',
+      'can_create' => FALSE,
+    ),
+    'mysql' => array(
+      'type_name' => t('MySQL Database'),
+      'description' => t('Import the backup directly into another MySQL database. Database sources can also be used as a source to backup from.'),
+      'file' => drupal_get_path('module', 'backup_migrate') . '/includes/sources.db.mysql.inc',
+      'class' => 'backup_migrate_source_db_mysql',
+      'can_create' => TRUE,
+    ),
+    'filesource' => array(
+      'description' => t('A files directory which can be backed up from.'),
+      'file' => drupal_get_path('module', 'backup_migrate') . '/includes/sources.filesource.inc',
+      'class' => 'backup_migrate_destination_filesource',
+      'type_name' => t('File Directory'),
+      'can_create' => TRUE,
+    ),
+    'archive' => array(
+      'description' => t('Create an archive of your entire site.'),
+      'file' => drupal_get_path('module', 'backup_migrate') . '/includes/sources.archivesource.inc',
+      'class' => 'backup_migrate_files_destination_archivesource',
+      'type_name' => t('Site Archive'),
+      'can_create' => FALSE,
+    ),
+  );
+
+  return $out;
+}
+
+/**
+ * Implements hook_backup_migrate_sources().
+ *
+ * Get the built in backup sources and those in the db.
+ */
+function backup_migrate_backup_migrate_sources() {
+  require_once dirname(__FILE__) . '/filters.inc';
+
+  // Expose the configured databases as sources.
+  $out = backup_migrate_filters_invoke_all('sources');
+
+  return $out;
+}
+
+/**
+ * Get the source options as a form element.
+ */
+function _backup_migrate_get_source_form($source_id = 'db') {
+  require_once dirname(__FILE__) . '/destinations.inc';
+
+  $form = array();
+  $sources = _backup_migrate_get_source_pulldown($source_id);
+  if (count($sources['#options']) > 1) {
+    $form['source'] = array(
+      "#type" => "fieldset",
+      "#title" => t("Backup Source"),
+      "#collapsible" => TRUE,
+      "#collapsed" => FALSE,
+      "#tree" => FALSE,
+    );
+    $sources['#description'] = t("Choose the database to backup. Any database destinations you have created and any databases specified in your settings.php can be backed up.");
+
+    $form['source']['source_id'] = $sources;
+  }
+  else {
+    $form = array();
+    $form['source']['source_id'] = array(
+      "#type" => "value",
+      "#value" => $source_id,
+    );
+  }
+  return $form;
+}
+
+/**
+ * Get pulldown to select existing source options.
+ */
+function _backup_migrate_get_source_pulldown($source_id = NULL) {
+  $sources = _backup_migrate_get_source_form_item_options();
+  $form = array(
+    "#type" => "select",
+    "#title" => t("Backup Source"),
+    "#options" => _backup_migrate_get_source_form_item_options(),
+    "#default_value" => $source_id,
+  );
+  return $form;
+}
+
+/**
+ * Get the location options as an options array for a form item.
+ */
+function _backup_migrate_get_source_form_item_options() {
+  $out = array();
+  foreach (backup_migrate_get_sources() as $key => $location) {
+    $out[$key] = $location->get_name();
+  }
+  return $out;
+}
+
+/**
+ * A base class for creating sources.
+ */
+class backup_migrate_source extends backup_migrate_location {
+  public $db_table = "backup_migrate_sources";
+  public $type_name = 'source';
+  public $singular = 'source';
+  public $plural = 'sources';
+  public $title_plural = 'Sources';
+  public $title_singular = 'Source';
+
+  /**
+   * This function is not supposed to be called.
+   *
+   * It is just here to help out the po extractor.
+   */
+  public function strings() {
+    // Help the pot extractor find these strings.
+    t('source');
+    t('sources');
+    t('Sources');
+    t('Source');
+  }
+
+  /**
+   * Get the available location types.
+   */
+  public function location_types() {
+    return backup_migrate_get_source_subtypes();
+  }
+
+}
+
+/**
+ * A base class for creating sources.
+ */
+class backup_migrate_source_remote extends backup_migrate_source {
+
+  /**
+   * The location is a URI so parse it and store the parts.
+   */
+  public function get_location() {
+    return $this->url(FALSE);
+  }
+
+  /**
+   * The location to display is the url without the password.
+   */
+  public function get_display_location() {
+    return $this->url(TRUE);
+  }
+
+  /**
+   * Returns the location with the password.
+   */
+  public function set_location($location) {
+    $this->location = $location;
+    $this->set_url($location);
+  }
+
+  /**
+   * Source configuration callback.
+   */
+  public function edit_form() {
+    $form = parent::edit_form();
+    $form['scheme'] = array(
+      "#type" => "textfield",
+      "#title" => t("Scheme"),
+      "#default_value" => @$this->dest_url['scheme'] ? $this->dest_url['scheme'] : '',
+      "#required" => TRUE,
+      "#weight" => 0,
+    );
+    $form['host'] = array(
+      "#type" => "textfield",
+      "#title" => t("Host"),
+      "#default_value" => @$this->dest_url['host'] ? $this->dest_url['host'] : 'localhost',
+      "#required" => TRUE,
+      "#weight" => 10,
+    );
+    $form['path'] = array(
+      "#type" => "textfield",
+      "#title" => t("Path"),
+      "#default_value" => @$this->dest_url['path'],
+      "#required" => TRUE,
+      "#weight" => 20,
+    );
+    $form['user'] = array(
+      "#type" => "textfield",
+      "#title" => t("Username"),
+      "#default_value" => @$this->dest_url['user'],
+      "#required" => TRUE,
+      "#weight" => 30,
+    );
+    $form['pass'] = array(
+      "#type" => "password",
+      "#title" => t("Password"),
+      "#default_value" => @$this->dest_url['pass'],
+      '#description' => '',
+      "#weight" => 40,
+    );
+    if (@$this->dest_url['pass']) {
+      $form['old_password'] = array(
+        "#type" => "value",
+        "#value" => @$this->dest_url['pass'],
+      );
+      $form['pass']["#description"] .= t('You do not need to enter a password unless you wish to change the currently saved password.');
+    }
+    return $form;
+  }
+
+  /**
+   * Submits the configuration form.
+   *
+   * Glue the url together and add the old password back if a new one was not
+   * specified.
+   */
+  public function edit_form_submit($form, &$form_state) {
+    $form_state['values']['pass'] = $form_state['values']['pass'] ? $form_state['values']['pass'] : $form_state['values']['old_password'];
+    $form_state['values']['location'] = $this->glue_url($form_state['values'], FALSE);
+    parent::edit_form_submit($form, $form_state);
+  }
+
+}

+ 301 - 0
sites/all/modules/contrib/admin/backup_migrate/tests/BmTestBase.test

@@ -0,0 +1,301 @@
+<?php
+
+/**
+ * @file
+ * Shared functionality to make the rest of the tests simpler.
+ */
+
+/**
+ * Base class for testing a module's custom tags.
+ */
+abstract class BmTestBase extends DrupalWebTestCase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp(array $modules = array()) {
+    $modules[] = 'backup_migrate';
+    parent::setUp($modules);
+  }
+
+  /**
+   * Log in as user 1.
+   *
+   * The benefit of doing this is that it ignores permissions entirely, so the
+   * raw functionality can be tested.
+   */
+  protected function loginUser1() {
+    // Load user 1.
+    $account = user_load(1, TRUE);
+
+    // Reset the password.
+    $password = user_password();
+    $edit = array(
+      'pass' => $password,
+    );
+    user_save($account, $edit);
+    $account->pass_raw = $password;
+
+    // Login.
+    $this->drupalLogin($account);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function verbose($message, $title = NULL) {
+    // Handle arrays, objects, etc.
+    if (!is_string($message)) {
+      $message = "<pre>\n" . print_r($message, TRUE) . "\n</pre>\n";
+    }
+
+    // Optional title to go before the output.
+    if (!empty($title)) {
+      $title = '<h2>' . check_plain($title) . "</h2>\n";
+    }
+
+    parent::verbose($title . $message);
+  }
+
+  /**
+   * Confirm that a selector has the expected items.
+   */
+  protected function assertSelectOptions($select_id, array $options, $message = '') {
+    $elements = $this
+      ->xpath('//select[@id=:id]//option', array(
+        ':id' => $select_id,
+      ));
+    $results = $this->assertEqual(count($elements), count($options), t('The same number of items were found as were requested'));
+    $this->verbose($elements);
+
+    foreach ($options as $option) {
+      $elements = $this
+        ->xpath('//select[@id=:id]//option[@value=:option]', array(
+          ':id' => $select_id,
+          ':option' => $option,
+        ));
+      $this->verbose($elements);
+      $results *= $this->assertTrue(isset($elements[0]), $message ? $message : t('Option @option for field @id is present.', array(
+        '@option' => $option,
+        '@id' => $select_id,
+      )), t('Browser'));
+    }
+
+    return $results;
+  }
+
+  /**
+   * Confirm that a specific selector does not have items selected.
+   */
+  protected function assertNoOptionsSelected($id, $message = '') {
+    $elements = $this
+      ->xpath('//select[@id=:id]//option[@selected="selected"]', array(
+        ':id' => $id,
+      ));
+    return $this
+      ->assertTrue(!isset($elements[0]), $message ? $message : t('Field @id does not have any selected items.', array(
+        '@id' => $id,
+      )), t('Browser'));
+  }
+
+  /**
+   * Work out which compressor systems are supported by PHP.
+   *
+   * @return array
+   *   The list of supported compressors. Will always include the item 'none'.
+   */
+  protected function supportedCompressors() {
+    $items = array('none');
+
+    // Work out which systems are supported.
+    if (@function_exists("gzencode")) {
+      $items[] = 'gzip';
+    }
+    if (@function_exists("bzcompress")) {
+      $items[] = 'bzip';
+    }
+    if (class_exists('ZipArchive')) {
+      $items[] = 'zip';
+    }
+
+    return $items;
+  }
+
+  /**
+   * Get a list of the files in a specific destination.
+   *
+   * @param string $destination_id
+   *   The ID of the destination to check. Defaults to the manual file path.
+   *
+   * @return array
+   *   The backup files found in the requested backup destination.
+   */
+  protected function listBackupFiles($destination_id = 'manual') {
+    require_once dirname(__FILE__) . '/../includes/destinations.inc';
+
+    $items = array();
+
+    // Load the destination object.
+    $destination = backup_migrate_get_destination($destination_id);
+    if (!empty($destination)) {
+      $items = $destination->list_files();
+    }
+
+    return $items;
+  }
+
+  /**
+   * Run a specific backup.
+   *
+   * @param string $destination_id
+   *   The ID of the destination to check. Defaults to the manual file path.
+   * @param string $source_id
+   *   The ID of the source to check. Defaults to the database.
+   */
+  protected function runBackup($destination_id = 'manual', $source_id = 'db') {
+    $this->drupalGet(BACKUP_MIGRATE_MENU_PATH);
+    $this->assertResponse(200);
+    $edit = array(
+      'destination_id' => $destination_id,
+      'source_id' => $source_id,
+    );
+    $this->drupalPost(NULL, $edit, 'Backup now');
+    $this->assertResponse(200);
+    // Confirm the response is as expected. This is split up into separate
+    // pieces because it'd be more effort than is necessary right now to confirm
+    // what the exact filename is.
+    if ($source_id === 'db') {
+      $this->assertText('Default Database backed up successfully');
+    }
+    else {
+      $this->assertText(' backed up successfully');
+    }
+    $this->assertText('in destination');
+    if ($source_id === 'db') {
+      $this->assertLink('download');
+      $this->assertLink('restore');
+      $this->assertLink('delete');
+    }
+  }
+
+  /**
+   * Delete all of the files in a specific backup destination.
+   *
+   * @param string $destination_id
+   *   The ID of the destination to check. Defaults to the manual file path.
+   */
+  protected function deleteBackups($destination_id = 'manual') {
+    $destination = backup_migrate_get_destination($destination_id);
+    $files = $this->listBackupFiles($destination_id);
+    if (!empty($files)) {
+      foreach ($files as $file_id => $file) {
+        $destination->delete_file($file_id);
+      }
+    }
+  }
+
+  /**
+   * Work out whether a backup filename includes a timestamp.
+   *
+   * @param object $file
+   *   The backup file to examine.
+   *
+   * @return mixed
+   *   Returns 1 if found, 0 if not found, FALSE if an error occurs.
+   */
+  protected function fileHasTimestamp($file) {
+    require_once dirname(__FILE__) . '/../includes/files.inc';
+
+    // Get the default filename, this is used later.
+    $default_filename = _backup_migrate_default_filename();
+    $ext = implode('.', $file->ext);
+    $pattern = "/{$default_filename}-(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d)-(\d\d)-(\d\d).{$ext}/";
+
+    return preg_match($pattern, $file->file_info['filename']);
+  }
+
+  /**
+   * Confirm that a backup filename includes a timestamp.
+   *
+   * @param object $file
+   *   The backup file to examine.
+   *
+   * @return bool
+   *   Indicates whether the file includes a timestamp.
+   */
+  protected function assertFileTimestamp($file) {
+    return $this->assertTrue($this->fileHasTimestamp($file));
+  }
+
+  /**
+   * Confirm that a backup filename does not include a timestamp.
+   *
+   * @param object $file
+   *   The backup file to examine.
+   *
+   * @return bool
+   *   Indicates whether the file does not include a timestamp.
+   */
+  protected function assertNoFileTimestamp($file) {
+    return !$this->assertFalse($this->fileHasTimestamp($file));
+  }
+
+  /**
+   * Get a profile.
+   *
+   * @param string $profile_id
+   *   The name of the profile to load. Defaults to 'default'.
+   *
+   * @return object
+   *   The profile object.
+   */
+  protected function getProfile($profile_id = 'default') {
+    require_once dirname(__FILE__) . '/../includes/profiles.inc';
+
+    return backup_migrate_get_profile($profile_id);
+  }
+
+  /**
+   * Confirm the most recently sent e-mail(s) contain the expected string.
+   *
+   * @param string $field_name
+   *   Name of field or message property to assert: subject, body, id, etc.
+   * @param string $string
+   *   String to search for.
+   * @param int $expected
+   *   Number of times $string should occur in the field.
+   * @param int $email_depth
+   *   Number of emails to search for string, starting with most recent.
+   *
+   * @return bool
+   *   TRUE on pass, FALSE on fail.
+   *
+   * @see DrupalWebTestCase::assertMailString()
+   */
+  protected function assertMailStringCount($field_name, $string, $expected, $email_depth) {
+    $mails = $this->drupalGetMails();
+    $number_of_mails = count($mails);
+    $string_count = 0;
+
+    for ($i = $number_of_mails - 1; $i >= $number_of_mails - $email_depth && $i >= 0; $i--) {
+      $mail = $mails[$i];
+
+      // Normalize whitespace, as it isn't eacy to know what the mail system
+      // might have done. Any run of whitespace becomes a single space.
+      $normalized_mail = preg_replace('/\\s+/', ' ', $mail[$field_name]);
+      $normalized_string = preg_replace('/\\s+/', ' ', $string);
+      $string_count += substr_count($normalized_mail, $normalized_string);
+    }
+
+    $mail_count = min($number_of_mails, $email_depth);
+    $message = format_plural($mail_count, 'Text "@needle" was found @stringcount times in field "@field" of the most recent email message.', 'Text "@needle" was found @stringcount times in field "@field" of the @mailcount most recent email messages.', array(
+      '@needle' => $string,
+      '@stringcount' => $string_count,
+      '@field' => $field_name,
+      '@mailcount' => $mail_count,
+    ));
+
+    return $this->assertTrue(($string_count === $expected), $message);
+  }
+
+}

+ 276 - 0
sites/all/modules/contrib/admin/backup_migrate/tests/BmTestBasics.test

@@ -0,0 +1,276 @@
+<?php
+
+/**
+ * @file
+ * Tests for different parts of the Backup Migrate system.
+ */
+
+/**
+ * Test that the front page still loads.
+ */
+class BmTestBasics extends BmTestBase {
+
+  /**
+   * Define this test class.
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Basic tests',
+      'description' => 'Run through basic scenarios and functionality.',
+      'group' => 'backup_migrate',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp(array $modules = array()) {
+    parent::setUp($modules);
+
+    // Log in as user 1, so that permissions are irrelevant.
+    $this->loginUser1();
+  }
+
+  /**
+   * Verify the main page has the expected form, then run a basic backup.
+   */
+  public function testQuickBackup() {
+    // Ensure that the private file system is working correctly.
+    $this->drupalGet('admin/config/media/file-system');
+    $this->assertResponse(200);
+    $edit = array();
+    $this->drupalPost(NULL, $edit, 'Save configuration');
+    $this->assertResponse(200);
+    $this->assertText('The configuration options have been saved.');
+
+    // Load the main B&M page.
+    $this->drupalGet(BACKUP_MIGRATE_MENU_PATH);
+    $this->assertResponse(200);
+
+    // @todo Confirm each of the tabs are present.
+    // @todo Confirm each of the local tasks are present.
+    // Confirm the form has the expected fields.
+    $this->assertFieldByName('source_id');
+    $this->assertFieldByName('destination_id');
+    $this->assertFieldByName('profile_id');
+    $this->assertFieldByName('copy');
+    $this->assertFieldByName('copy_destination_id');
+    $this->assertFieldByName('description_enabled');
+    // This item should not have a value "selected", it just defaults to the
+    // first item being the active item.
+    $items = array('db', 'files', 'archive');
+    $this->assertSelectOptions('edit-source-id', $items);
+    $this->assertNoOptionsSelected('edit-source-id');
+    // This item should have a value "selected", not just the first item. Note:
+    // if the 'manual' backup option isn't available then the private directory
+    // path is not set correctly.
+    $items = array('manual', 'download');
+    $this->assertSelectOptions('edit-destination-id', $items);
+    $this->assertOptionSelected('edit-destination-id', 'download');
+    // This item should not have a value "selected", it just defaults to the
+    // first item being the active item.
+    $items = array('default');
+    $this->assertSelectOptions('edit-profile-id', $items);
+    $this->assertNoOptionsSelected('edit-profile-id');
+    // This item should not have a value "selected", it just defaults to the
+    // first item being the active item.
+    $items = array('manual', 'download');
+    $this->assertSelectOptions('edit-copy-destination-id', $items);
+    $this->assertNoOptionsSelected('edit-copy-destination-id');
+
+    // Generate a backup and confirm it was created correctly.
+    $edit = array(
+      'destination_id' => 'manual',
+    );
+    $this->drupalPost(NULL, $edit, 'Backup now');
+    $this->assertResponse(200);
+    // Confirm the response is as expected. This is split up into separate
+    // pieces because it'd be more effort than is necessary right now to confirm
+    // what the exact filename is.
+    $this->assertText('Default Database backed up successfully');
+    $this->assertText('in destination Manual Backups Directory');
+    $this->assertLink('download');
+    $this->assertLink('restore');
+    $this->assertLink('delete');
+
+    // Try requesting the backup file.
+    $xpath = $this
+      ->xpath('//a[normalize-space(text())=:label]', array(
+        ':label' => 'download',
+      ));
+    $this->verbose($xpath);
+    $this->assertTrue(isset($xpath[0]['href']));
+    $this->assertNotNull($xpath[0]['href']);
+    // @todo This doesn't work on drupalci, so work out how to fix it.
+    // $this->drupalGet($xpath[0]['href']);
+    $this->assertResponse(200);
+  }
+
+  /**
+   * Test the custom validators.
+   */
+  public function testFieldValidators() {
+    // Need this file loaded for the custom validators.
+    module_load_include('advanced_settings.inc', 'backup_migrate');
+
+    $field = 'mock_field';
+
+    $element = array(
+      '#value' => 'a',
+      '#title' => '',
+      '#parents' => array($field),
+    );
+
+    // Test the memory limit validator.
+    $element['#title'] = 'Mock Field (Memory Limit)';
+    $test_values = array(
+      // Value to be tested => validity.
+      // Special meaning: no limit.
+      '-1' => TRUE,
+      '50' => TRUE,
+      // 5 megabytes.
+      '5M' => TRUE,
+      // 500 kilobytes.
+      '.5M' => TRUE,
+      // 5 gigabytes.
+      '5G' => TRUE,
+      '.5G' => TRUE,
+      '1.5G' => TRUE,
+      '0' => TRUE,
+      'a' => FALSE,
+      '-2' => FALSE,
+      // You cannot have half a byte.
+      '1.5' => FALSE,
+      '5T' => FALSE,
+      '5 G' => FALSE,
+      '.5.5G' => FALSE,
+    );
+    foreach ($test_values as $value => $valid) {
+      $this->assertValidFieldValue('backup_migrate_memory_limit_validate', $element, $value, $valid, 'memory limit value');
+    }
+
+    // Test the positive non-zero integer validator.
+    $element['#title'] = 'Mock Field (Unsigned Integer)';
+    $test_values = array(
+      '0' => TRUE,
+      '1' => TRUE,
+      '10000' => TRUE,
+      // NaN.
+      'a' => FALSE,
+      // Not an integer.
+      '.5' => FALSE,
+      // Not positive.
+      '-1' => FALSE,
+      // Largest integer.
+      // @code
+      // PHP_INT_MAX => TRUE,
+      // @endcode
+      // Resolves to a decimal.
+      // @code
+      // (PHP_INT_MAX + 1) => FALSE,
+      // @endcode
+    );
+    foreach ($test_values as $value => $valid) {
+      $this->assertValidFieldValue('backup_migrate_unsigned_integer_validate', $element, $value, $valid, 'zero or a positive integer');
+    }
+
+    // Test the validator for decimals ranging from 0 to 1.
+    $element['#title'] = 'Mock Field (Decimal between 0 and 1)';
+    $test_values = array(
+      '0' => TRUE,
+      '1' => TRUE,
+      '0.1' => TRUE,
+      '.1' => TRUE,
+      '1.1' => FALSE,
+      '-1' => FALSE,
+      '-1.1' => FALSE,
+      '2' => FALSE,
+      'a' => FALSE,
+    );
+    foreach ($test_values as $value => $valid) {
+      $this->assertValidFieldValue('backup_migrate_fraction_validate', $element, $value, $valid, 'decimal between 0 and 1');
+    }
+  }
+
+  /**
+   * Asserts that the content of a field passes its associated validation.
+   *
+   * @param string $validator
+   *   Drupal validator function callback.
+   * @param array $element
+   *   Input for error setter.
+   * @param string $value
+   *   Field value to be tested.
+   * @param bool $value_is_valid
+   *   Expected outcome.
+   * @param string $type
+   *   Description of value sub-type.
+   */
+  protected function assertValidFieldValue($validator, array $element, $value, $value_is_valid, $type) {
+    $form = array();
+    $form_state = array();
+    $field = 'mock_field';
+    $element['#value'] = $value;
+
+    form_clear_error();
+    call_user_func_array($validator, array($element, &$form_state, $form));
+    $errors = form_get_errors();
+
+    if ($value_is_valid) {
+      $args = array(
+        '%value' => $value,
+        '%type' => $type,
+      );
+      $msg = t('%value is a valid %type.', $args);
+      $result = $this->assertTrue(empty($errors[$field]), $msg);
+    }
+    else {
+      $args = array(
+        '%value' => $value,
+        '%type' => $type,
+        '%msg' => strip_tags($errors[$field]),
+      );
+      $msg = t('%value is not a valid %type. Error message: "%msg".', $args);
+      $result = $this->assertFalse(empty($errors[$field]), $msg);
+    }
+
+    return $result;
+  }
+
+  /**
+   * Confirm backup_migrate_to_bytes() works.
+   */
+  public function testBackupMigrateToBytes() {
+    // PHP manual:
+    // http://php.net/manual/en/ini.core.php#ini.memory-limit
+    // '-1' is a reserved value meaning 'no limit'.
+    $input = '-1';
+    $expected = -1;
+    $this->assertEqual(backup_migrate_to_bytes($input), $expected);
+
+    // PHP would halt before this, but let us test anyway.
+    $input = 'nonsense';
+    $expected = 0;
+    $this->assertEqual(backup_migrate_to_bytes($input), $expected);
+
+    // This is 123 bytes, but there is a bug in B&M that turns
+    // this into 12.5 megabytes.
+    $input = '123';
+    $expected = 12582912;
+    $this->assertEqual(backup_migrate_to_bytes($input), $expected);
+
+    // 45 megabytes.
+    $input = '45M';
+    $expected = 47185920;
+    $this->assertEqual(backup_migrate_to_bytes($input), $expected);
+
+    // 8 gigabytes.
+    $input = '8G';
+    $expected = 8589934592;
+    $this->assertEqual(backup_migrate_to_bytes($input), $expected);
+  }
+
+}
+
+// @todo Test permissions.
+// @todo Test admin forms.

+ 104 - 0
sites/all/modules/contrib/admin/backup_migrate/tests/BmTestCtools.test

@@ -0,0 +1,104 @@
+<?php
+
+/**
+ * @file
+ * Test extended functionality as provided by Ctools.
+ */
+
+/**
+ * Class for testing CTools' extended functionality.
+ */
+class BmTestCtools extends BmTestBase {
+
+  /**
+   * Define this test class.
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'CTools tests',
+      'description' => 'Test integration with the CTools module.',
+      'group' => 'backup_migrate',
+      'dependencies' => array('ctools'),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp(array $modules = array()) {
+    $modules[] = 'bm_test';
+    $modules[] = 'ctools';
+
+    parent::setUp($modules);
+
+    // Log in as user 1, so that permissions are irrelevant.
+    $this->loginUser1();
+  }
+
+  /**
+   * Confirm automated settings exist.
+   */
+  public function testSettingsPage() {
+    // Load the B&M Settings page.
+    $this->drupalGet(BACKUP_MIGRATE_MENU_PATH . '/settings');
+    $this->assertResponse(200);
+
+    // Does the mock schedule exist?
+    $this->assertText('Mock weekly database schedule');
+
+    // Does the mock source exist?
+    $this->assertText('Mock file directory');
+
+    // Does the mock destination exist?
+    $this->assertText('Mock e-mail destination');
+
+    // Load the B&M Schedule page.
+    $this->drupalGet(BACKUP_MIGRATE_MENU_PATH . '/schedule');
+    $this->assertResponse(200);
+
+    // Does the mock schedule exist?
+    $this->assertText('Mock weekly database schedule');
+
+    // Does the mock schedule contain the appropriate values?
+    $this->drupalGet(BACKUP_MIGRATE_MENU_PATH . '/schedule/edit/mock_db_weekly');
+    $this->assertResponse(200);
+
+    $fields = _bm_test_get_mock_schedule();
+
+    // @todo Deal with periods, but now I need sleep.
+    // See the get_frequency_period() method in includes/schedules.inc.
+    unset($fields['period']);
+
+    // Test the destination selection with its own assertion.
+    $this->assertOptionSelected('edit-destination-id', $fields['destination_id']);
+    unset($fields['destination_id']);
+
+    foreach ($fields as $key => $field) {
+      $id = 'edit-' . str_replace('_', '-', $key);
+      $this->assertFieldById($id, $field, 'Found field by id "' . $id . '" and value "' . $field . '".');
+    }
+
+    // Does the mock source contain the appropriate values?
+    $this->drupalGet(BACKUP_MIGRATE_MENU_PATH . '/settings/source/edit/mock_file_directory');
+    $this->assertResponse(200);
+
+    $fields = _bm_test_get_mock_source();
+    unset($fields['subtype']);
+    foreach ($fields as $key => $field) {
+      $id = 'edit-' . str_replace('_', '-', $key);
+      $this->assertFieldById($id, $field, 'Found field by id "' . $id . '" and value "' . $field . '".');
+    }
+
+    // Does the mock destination contain the appropriate values?
+    $this->drupalGet(BACKUP_MIGRATE_MENU_PATH . '/settings/destination/edit/mock_email');
+    $this->assertResponse(200);
+
+    $fields = _bm_test_get_mock_destination();
+    unset($fields['subtype']);
+    foreach ($fields as $key => $field) {
+      $id = 'edit-' . str_replace('_', '-', $key);
+      $this->assertFieldById($id, $field, 'Found field by id "' . $id . '" and value "' . $field . '".');
+    }
+  }
+
+}

+ 207 - 0
sites/all/modules/contrib/admin/backup_migrate/tests/BmTestEmail.test

@@ -0,0 +1,207 @@
+<?php
+
+/**
+ * @file
+ * Test email delivery.
+ */
+
+/**
+ * Test email delivery.
+ */
+class BmTestEmail extends BmTestBase {
+
+  /**
+   * Define this test class.
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Email handling',
+      'description' => 'Confirm email handling works as intended.',
+      'group' => 'backup_migrate',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp(array $modules = array()) {
+    parent::setUp($modules);
+
+    // Log in as user 1, so that permissions are irrelevant.
+    $this->loginUser1();
+  }
+
+  /**
+   * Confirm the whole email process.
+   */
+  public function testAddEmailDestination() {
+    // See if we can add a destination.
+    // Load the email destination Add page.
+    $this->drupalGet(BACKUP_MIGRATE_MENU_PATH . '/settings/destination/add/email');
+    $this->assertResponse(200);
+
+    // Verify all of the expected fields exist.
+    $this->assertFieldByName('name');
+    $this->assertFieldByName('name', 'Untitled Destination');
+    $this->assertFieldByName('machine_name');
+    $this->assertFieldByName('location');
+
+    // Set up test values.
+    $test_source = strtolower($this->randomName(16));
+    $address = strtolower($this->randomName(10)) . '@example.com';
+    $test_destination = strtolower($this->randomName(16));
+
+    // Submit e-mail destination form with invalid values.
+    $this->submitDestinationEmail('', $test_destination, $address);
+    $this->assertText(t('Destination name field is required.'), 'Name required.');
+
+    $this->submitDestinationEmail($this->randomString(16), '', $address);
+    $this->assertText(t('Machine-readable name field is required.'), 'Machine name required.');
+
+    $this->submitDestinationEmail($this->randomString(16), $test_destination, '');
+    $this->assertText(t('Email Address field is required.'), 'E-mail required.');
+
+    // Step 1, set-up.
+    // Create a test destination.
+    $this->submitDestinationEmail($this->randomString(16), $test_destination, $address);
+    $this->assertText(t('Your destination was saved'));
+
+    // Create a test source.
+    // @todo Test the source add form.
+    // @todo Maybe add a note somewhere about how we are using a small
+    // source on purpose, so as to not break the testing system.
+    $this->submitSourceFiles($this->randomString(16), $test_source, drupal_get_path('module', 'backup_migrate') . '/tests/files/');
+    $this->assertText(t('Your source was saved'));
+
+    // Run a manual back-up so we have a file to compare against.
+    $this->runBackup('manual', $test_source);
+
+    $files = $this->listBackupFiles();
+    $key = key($files);
+
+    // Confirm the back-up file exists.
+    $this->assertNotEqual($key, NULL, 'A backup file was found.');
+
+    $file = $files[$key];
+
+    // Copied from destinations.email.inc to build a compatible parameter.
+    $attachment = new stdClass();
+    $attachment->filename = $file->name;
+    $attachment->path = $file->path;
+
+    // Step 2, e-mail generated through the Quick Backup form.
+    // Run a backup per e-mail.
+    $this->runBackup($test_destination, $test_source);
+
+    // Confirm that an e-mail was sent.
+    $captured_emails = $this->drupalGetMails(array('id' => 'backup_migrate_destination_mail', 'to' => $address));
+    $this->verboseEmail();
+    $this->assertEqual(count($captured_emails), 1, 'A back-up was mailed.');
+
+    // Does the attachment occur in the captured e-mail?
+    // Note that the e-mail from Step 2 is tested against the back-up file from
+    // Step 1.
+    $file_data = fread(fopen($attachment->path, "r"), filesize($attachment->path));
+    $encoded_file_data = chunk_split(base64_encode($file_data), 70, "\r\n");
+    $this->assertMailString('body', $encoded_file_data, 1);
+
+    // Step 3, e-mail generated through the main helper function.
+    // Make sure the new $address is never the same as the old one.
+    $address = strtolower($this->randomName(1 + strlen($address))) . '@example.com';
+
+    _backup_migrate_destination_email_mail_backup($attachment, $address);
+
+    // Confirm that an e-mail is sent.
+    $captured_emails = $this->drupalGetMails(array('id' => 'backup_migrate_destination_mail', 'to' => $address));
+    $this->verboseEmail();
+    $this->assertEqual(count($captured_emails), 1, 'A back-up was mailed.');
+
+    // Check if the Subject field contains the filename.
+    $this->assertMailString('subject', $attachment->filename, 1);
+
+    // Check if the body contains the filename.
+    $this->assertMailString('body', $attachment->filename, 1);
+
+    // Step 4, (partial) e-mail generated through a mime_mail() object.
+    // Build an e-mail.
+    $test_mail = new mime_mail();
+    $boundary = "b" . md5(uniqid(time()));
+
+    // Make sure the new $address is never the same as the old one.
+    $address = strtolower($this->randomName(1 + strlen($address))) . '@example.com';
+
+    $attach = fread(fopen($attachment->path, "r"), filesize($attachment->path));
+    $test_mail->add_attachment($attach, $attachment->filename, "application/octet-stream");
+
+    $message = $test_mail->build_multipart($boundary);
+
+    $params = array(
+      'body' => $message,
+      'subject' => 'test',
+      'headers' => array(
+        'MIME-Version' => "1.0",
+        'Content-Type' => "multipart/mixed; boundary=\"$boundary\"",
+      ),
+    );
+
+    drupal_mail('backup_migrate', 'destination_mail', $address, '', $params);
+    $captured_emails = $this->drupalGetMails(array('id' => 'backup_migrate_destination_mail', 'to' => $address));
+    $this->verboseEmail();
+
+    // Confirm that an e-mail was sent.
+    $this->assertEqual(count($captured_emails), 1, 'A back-up was mailed.');
+
+    // Does the boundary occur in the captured e-mail?
+    $this->assertMailString('body', '--' . $boundary, 1);
+
+    // How many boundaries?
+    $expected = count($test_mail->parts) + 1;
+    $this->assertMailStringCount('body', '--' . $boundary, $expected, 1);
+
+    // Cleanup - purge all backups.
+    $this->deleteBackups();
+  }
+
+  /**
+   * Submits the destination form for E-mails.
+   *
+   * @param string $name
+   *   The name of the destination.
+   * @param string $machine_name
+   *   The machine name of the destination.
+   * @param string $mail
+   *   The e-mail address of the destination.
+   */
+  public function submitDestinationEmail($name, $machine_name, $mail) {
+    $this->drupalGet('admin/config/system/backup_migrate/settings/destination/add/email');
+    $this->assertResponse(200);
+    $edit = array();
+    $edit['name'] = $name;
+    $edit['machine_name'] = $machine_name;
+    $edit['location'] = $mail;
+    $this->drupalPost(NULL, $edit, t('Save destination'));
+    $this->assertResponse(200);
+  }
+
+  /**
+   * Submits the source form for Files.
+   *
+   * @param string $name
+   *   The name of the source.
+   * @param string $machine_name
+   *   The machine name of the source.
+   * @param string $path
+   *   The path of the source.
+   */
+  public function submitSourceFiles($name, $machine_name, $path) {
+    $this->drupalGet('admin/config/system/backup_migrate/settings/source/add/filesource');
+    $this->assertResponse(200);
+    $edit = array();
+    $edit['name'] = $name;
+    $edit['machine_name'] = $machine_name;
+    $edit['location'] = $path;
+    $this->drupalPost(NULL, $edit, t('Save source'));
+    $this->assertResponse(200);
+  }
+
+}

+ 170 - 0
sites/all/modules/contrib/admin/backup_migrate/tests/BmTestProfiles.test

@@ -0,0 +1,170 @@
+<?php
+
+/**
+ * @file
+ * Tests the profiles functionality.
+ */
+
+/**
+ * Test that the front page still loads.
+ */
+class BmTestProfiles extends BmTestBase {
+
+  /**
+   * Define this test class.
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Profiles tests',
+      'description' => 'Run through basic scenarios and functionality.',
+      'group' => 'backup_migrate',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp(array $modules = array()) {
+    parent::setUp($modules);
+
+    // Log in as user 1, so that permissions are irrelevant.
+    $this->loginUser1();
+  }
+
+  /**
+   * Verify the profile page has the expected functionality available.
+   */
+  public function testProfilePage() {
+    // Load the main B&M page.
+    $this->drupalGet(BACKUP_MIGRATE_MENU_PATH . '/settings');
+    $this->assertResponse(200);
+
+    // Confirm the page has the expected settings details.
+    $this->assertText('Settings Profiles');
+    $this->assertText('Default Settings');
+    $this->assertLink('Create a new settings profile');
+  }
+
+  /**
+   * Confirm adding a new backup process works.
+   */
+  public function testAddDefaultProfile() {
+    require_once dirname(__FILE__) . '/../includes/files.inc';
+    require_once dirname(__FILE__) . '/../includes/profiles.inc';
+
+    // Load the main B&M page.
+    $this->drupalGet(BACKUP_MIGRATE_MENU_PATH . '/settings/profile/add');
+    $this->assertResponse(200);
+
+    $filename = _backup_migrate_default_filename();
+    $defaults = _backup_migrate_profile_default_profile();
+
+    // Verify all of the expected fields exist.
+    $this->assertFieldByName('name');
+    $this->assertFieldByName('name', 'Untitled Profile');
+    $this->assertFieldByName('machine_name');
+    $this->assertFieldByName('filename');
+    $this->assertFieldByName('filename', $filename);
+    // @todo Confirm all of the expected options are present.
+    $this->assertFieldByName('append_timestamp');
+    $this->assertFieldByName('timestamp_format');
+    $this->assertFieldByName('timestamp_format', $defaults['timestamp_format']);
+    $this->assertFieldByName('filters[compression]');
+    $items = $this->supportedCompressors();
+    $this->assertSelectOptions('edit-filters-compression', $items);
+    $this->assertOptionSelected('edit-filters-compression', 'gzip');
+    $this->assertFieldByName('filters[sources][db][exclude_tables][]');
+    $this->assertFieldByName('filters[sources][db][nodata_tables][]');
+    $this->assertFieldByName('filters[sources][db][utils_lock_tables]');
+    $this->assertFieldByName('filters[sources][files][exclude_filepaths]');
+    $this->assertFieldByName('filters[sources][archive][exclude_filepaths]');
+    $this->assertFieldByName('filters[utils_site_offline]');
+    $this->assertFieldByName('filters[utils_site_offline_message]');
+    $this->assertFieldByName('filters[utils_description]');
+    $this->assertFieldByName('filters[use_cli]');
+    $this->assertFieldByName('filters[ignore_errors]');
+    $this->assertFieldByName('filters[notify_success_enable]');
+    $this->assertFieldByName('filters[notify_success_email]');
+    $this->assertFieldByName('filters[notify_failure_enable]');
+    $this->assertFieldByName('filters[notify_failure_email]');
+  }
+
+  /**
+   * Confirm the backup filename processes work as expected.
+   */
+  public function testFilenameOptions() {
+    // Load the profile. This will be interacted with directly because otherwise
+    // the number of form fields will likely make it impossible to execute
+    // properly due to the max_input_vars setting defaulting to 1000.
+    $profile = $this->getProfile();
+
+    // Run a backup.
+    $this->runBackup();
+
+    // Confirm that there is only one file and it has a timestamp of some sort.
+    $files1 = $this->listBackupFiles();
+    $this->verbose($files1);
+    $this->assertTrue(count($files1) === 1, 'One backup file was found.');
+    $this->assertFileTimestamp(array_shift($files1));
+
+    // Run another backup.
+    $this->runBackup();
+
+    // Confirm that there are two backup files.
+    $files1b = $this->listBackupFiles();
+    $this->verbose($files1b);
+    $this->assertTrue(count($files1b) === 2, 'Two backup files were found.');
+
+    // Cleanup before the next test - purge existing backups.
+    $this->deleteBackups();
+
+    // Change settings to "create separate backups".
+    $profile->append_timestamp = 0;
+    $profile->save();
+
+    // Run a backup.
+    $this->runBackup();
+
+    // Confirm that separate files are retained.
+    $files2 = $this->listBackupFiles();
+    $this->verbose($files2);
+    $this->assertTrue(count($files2) === 1, 'One backup file was found.');
+    $this->assertNoFileTimestamp(array_shift($files2));
+
+    // Run another backup.
+    $this->runBackup();
+
+    // Confirm that separate files are retained.
+    $files2b = $this->listBackupFiles();
+    $this->verbose($files2b);
+    $this->assertTrue(count($files2b) === 2, 'Two backup files were found.');
+
+    // Cleanup before the next test - purge existing backups.
+    $this->deleteBackups();
+
+    // Change settings to "overwrite".
+    $profile->append_timestamp = 2;
+    $profile->save();
+
+    // Run a backup.
+    $this->runBackup();
+
+    // Confirm that a new file was created.
+    $files3 = $this->listBackupFiles();
+    $this->verbose($files3);
+    $this->assertTrue(count($files3) === 1, 'One backup file was found.');
+    $this->assertNoFileTimestamp(array_shift($files3));
+
+    // Run the backup again.
+    $this->runBackup();
+
+    // Confirm that a new file was not created.
+    $files3b = $this->listBackupFiles();
+    $this->verbose($files3b);
+    $this->assertTrue(count($files3b) === 1, 'One backup file was found.');
+
+    // Cleanup - purge all backups.
+    $this->deleteBackups();
+  }
+
+}

+ 141 - 0
sites/all/modules/contrib/admin/backup_migrate/tests/BmTestUpdate7310.test

@@ -0,0 +1,141 @@
+<?php
+
+/**
+ * @file
+ * Test module updates.
+ */
+
+/**
+ * Test module update 7310.
+ */
+class BmTestUpdate7310 extends BmTestBase {
+
+  /**
+   * Define this test class.
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Update 7310',
+      'description' => 'Confirm update 7310 works as intended.',
+      'group' => 'backup_migrate',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp(array $modules = array()) {
+    parent::setUp($modules);
+
+    // Log in as user 1, so that permissions are irrelevant.
+    $this->loginUser1();
+  }
+
+  /**
+   * Test update 7310.
+   */
+  public function testUpdate7310() {
+    require_once dirname(__FILE__) . '/../includes/destinations.inc';
+    require_once dirname(__FILE__) . '/../includes/schedules.inc';
+
+    $this->assertEqual(function_exists('backup_migrate_update_7310'), TRUE, 'Update 7310 exists.');
+
+    // First test what happens when there are no e-mail destinations.
+    // Execute the update function.
+    $result = backup_migrate_update_7310();
+
+    // We should be getting a notice as a result.
+    $message = "The following notice was displayed: &quot;$result&quot;";
+    $expected = 'No destinations were affected by this change.';
+    $this->assertEqual($expected, $result, $message);
+
+    // Next, add e-mail destinations and see what happens then.
+    // Create two mock e-mail destinations.
+    $address = strtolower($this->randomName(10)) . '@example.com';
+    $email_destination_id = strtolower($this->randomName(16));
+    $this->submitDestinationEmail('Mock e-mail destination', $email_destination_id, $address);
+    $address = strtolower($this->randomName(10)) . '@example.com';
+    $email_destination_id = strtolower($this->randomName(16));
+    $this->submitDestinationEmail('Mock e-mail destination', $email_destination_id, $address);
+    $this->assertText(t('Your destination was saved'));
+
+    // Create mock schedules with different types of destinations.
+    $mock_schedule_1 = $this->randomName(10);
+    $mock_schedule_2 = $this->randomName(10);
+    $mock_schedule_3 = $this->randomName(10);
+    $this->submitSchedule($mock_schedule_1, 'test_1', 'scheduled', '');
+    $this->assertText(t('Your schedule was saved'));
+    $this->submitSchedule($mock_schedule_2, 'test_2', $email_destination_id, '');
+    $this->assertText(t('Your schedule was saved'));
+    $this->submitSchedule($mock_schedule_3, 'test_3', 'scheduled', $email_destination_id);
+    $this->assertText(t('Your schedule was saved'));
+
+    $destinations = db_select('backup_migrate_destinations', 'bmd')->fields('bmd', array('machine_name'))->condition('subtype', 'email', '=')->execute()->fetchAllAssoc('machine_name', PDO::FETCH_ASSOC);
+    $destinations = array_keys($destinations);
+
+    // Execute the update function.
+    $result = backup_migrate_update_7310();
+
+    // We should be getting a notice as a result.
+    $expected = htmlspecialchars("Schedules that back up to e-mail destinations have been disabled. Check that you are using the correct e-mail addresses, then re-enable manually. The following schedules have been disabled: <ul><li>{$mock_schedule_2}</li><li>{$mock_schedule_3}</li></ul>");
+    $message = "The following notice was displayed: &quot;$result&quot;.";
+    $this->assertEqual($expected, $result, $message);
+
+    $schedules_query = db_select('backup_migrate_schedules', 'bms')->fields('bms', array('machine_name', 'enabled'));
+    $schedules = $schedules_query->execute()->fetchAllAssoc('machine_name', PDO::FETCH_ASSOC);
+
+    // Check that the correct values have changed.
+    $this->assertEqual($schedules['test_1']['enabled'], 1, 'The file back-up schedule remained enabled.');
+    $this->assertEqual($schedules['test_2']['enabled'], 0, 'The e-mail back-up schedule was disabled.');
+    $this->assertEqual($schedules['test_2']['enabled'], 0, 'The file back-up schedule with an e-mail copy was disabled.');
+  }
+
+  /**
+   * Submits the destination form for E-mails.
+   *
+   * @param string $name
+   *   The name of the destination.
+   * @param string $machine_name
+   *   The machine name of the destination.
+   * @param string $mail
+   *   The e-mail address of the destination.
+   */
+  public function submitDestinationEmail($name, $machine_name, $mail) {
+    $this->drupalGet('admin/config/system/backup_migrate/settings/destination/add/email');
+    $this->assertResponse(200);
+    $edit = array();
+    $edit['name'] = $name;
+    $edit['machine_name'] = $machine_name;
+    $edit['location'] = $mail;
+    $this->drupalPost(NULL, $edit, t('Save destination'));
+    $this->assertResponse(200);
+  }
+
+  /**
+   * Submits the schedule form.
+   *
+   * @param string $name
+   *   The name of the destination.
+   * @param string $machine_name
+   *   The machine name of the destination.
+   * @param string $destination_id
+   *   The destination ID to use.
+   * @param string $copy_destination_id
+   *   The destination ID to use for an optional copy.
+   */
+  public function submitSchedule($name, $machine_name, $destination_id, $copy_destination_id = NULL) {
+    $this->drupalGet('admin/config/system/backup_migrate/schedule/add');
+    $this->assertResponse(200);
+    $edit = array();
+    $edit['name'] = $name;
+    $edit['machine_name'] = $machine_name;
+    $edit['destination_id'] = $destination_id;
+    if (!empty($copy_destination_id)) {
+      $edit['copy'] = TRUE;
+      $edit['copy_destination_id'] = $copy_destination_id;
+    }
+    $this->drupalPost(NULL, $edit, t('Save schedule'));
+    $this->assertResponse(200);
+  }
+
+}

+ 13 - 0
sites/all/modules/contrib/admin/backup_migrate/tests/bm_test.info

@@ -0,0 +1,13 @@
+name = "Backup and Migrate Mock Module"
+description = "Mock module for testing the Backup and Migrate module."
+package = Other
+core = 7.x
+hidden = TRUE
+dependencies[] = simpletest
+dependencies[] = backup_migrate
+
+; Information added by Drupal.org packaging script on 2020-07-15
+version = "7.x-3.9"
+core = "7.x"
+project = "backup_migrate"
+datestamp = "1594807678"

+ 121 - 0
sites/all/modules/contrib/admin/backup_migrate/tests/bm_test.module

@@ -0,0 +1,121 @@
+<?php
+
+/**
+ * @file
+ * Mock module to help and test hooks.
+ */
+
+/**
+ * A base class for mock exportables.
+ */
+class BmTestItem {
+
+  /**
+   * Edit this to true to make a default item disabled initially.
+   *
+   * @var bool
+   */
+  public $disabled = FALSE;
+
+  /**
+   * Indicates the API version to use.
+   *
+   * @var int
+   */
+  public $api_version = 1;
+
+}
+
+/**
+ * Implements hook_ctools_plugin_api().
+ */
+function bm_test_ctools_plugin_api($module = NULL, $api = NULL) {
+  if ($module == "backup_migrate" && $api == "backup_migrate_exportables") {
+    return array("version" => "1");
+  }
+}
+
+/**
+ * Implements hook_exportables_backup_migrate_schedules().
+ */
+function bm_test_exportables_backup_migrate_schedules() {
+  $export = array();
+  $item = new BmTestItem();
+  $mock_record = _bm_test_get_mock_schedule();
+  foreach ($mock_record as $key => $field) {
+    $item->$key = $field;
+  }
+  $export['mock_db_weekly'] = $item;
+  return $export;
+}
+
+/**
+ * Helper function to create a mock schedule.
+ */
+function _bm_test_get_mock_schedule() {
+  return array(
+    'machine_name' => 'mock_db_weekly',
+    'name' => 'Mock weekly database schedule',
+    'source_id' => 'db',
+    'destination_id' => 'scheduled',
+    'copy_destination_id' => '',
+    'profile_id' => 'default',
+    'keep' => 4,
+    'period' => 604800,
+    'enabled' => TRUE,
+    'cron' => 'builtin',
+    'cron_schedule' => '0 4 * * *',
+  );
+}
+
+/**
+ * Implements hook_exportables_backup_migrate_sources().
+ */
+function bm_test_exportables_backup_migrate_sources() {
+  $export = array();
+  $item = new BmTestItem();
+  $mock_record = _bm_test_get_mock_source();
+  foreach ($mock_record as $key => $field) {
+    $item->$key = $field;
+  }
+  $export['mock_file_directory'] = $item;
+  return $export;
+}
+
+/**
+ * Helper function to create a mock source.
+ */
+function _bm_test_get_mock_source() {
+  return array(
+    'machine_name' => 'mock_file_directory',
+    'name' => 'Mock file directory',
+    'location' => '/dev/null',
+    'subtype' => 'filesource',
+  );
+}
+
+/**
+ * Implements hook_exportables_backup_migrate_destinations().
+ */
+function bm_test_exportables_backup_migrate_destinations() {
+  $export = array();
+  $item = new BmTestItem();
+  $mock_record = _bm_test_get_mock_destination();
+  foreach ($mock_record as $key => $field) {
+    $item->$key = $field;
+  }
+  $export['mock_email'] = $item;
+  return $export;
+}
+
+/**
+ * Helper function to create a mock destination.
+ */
+function _bm_test_get_mock_destination() {
+  return array(
+    'machine_name' => 'mock_email',
+    'name' => 'Mock e-mail destination',
+    'location' => 'test@example.com',
+    'subtype' => 'email',
+  );
+}

+ 7 - 0
sites/all/modules/contrib/admin/backup_migrate/tests/files/test.txt

@@ -0,0 +1,7 @@
+—Nay, Traveller! rest. This lonely yew-tree stands
+Far from all human dwelling: what if here
+No sparkling rivulet spread the verdant herb;
+What if these barren boughs the bee not loves;
+Yet, if the wind breathe soft, the curling waves,
+That break against the shore, shall lull thy mind
+By one soft impulse saved from vacancy.

+ 55 - 27
sites/all/modules/contrib/admin/google_analytics/README.txt

@@ -1,6 +1,6 @@
 
 Module: Google Analytics
-Author: Alexander Hass <http://drupal.org/user/85918>
+Author: Alexander Hass <https://drupal.org/user/85918>
 
 
 Description
@@ -12,12 +12,20 @@ Requirements
 
 * Google Analytics user account
 
-
 Installation
 ============
-* Copy the 'googleanalytics' module directory in to your Drupal
+Copy the 'googleanalytics' module directory in to your Drupal
 sites/all/modules directory as usual.
 
+Upgrading from 6.x-3.x and 7.x-1.x
+==================================
+If you upgrade from 6.x-3.x and 7.x-1.x (ga.js) to 7.x-2.x (analytics.js) you
+should verify if you used custom variables. Write down your settings or make a 
+screenshot. You need to re-configure the settings to use custom dimensions or
+metrics. There is no automatic upgrade path for custom variables feature. All
+other module settings are upgraded automatically.
+
+See https://support.google.com/analytics/answer/2795983?hl=en for more details.
 
 Usage
 =====
@@ -27,12 +35,8 @@ All pages will now have the required JavaScript added to the
 HTML footer can confirm this by viewing the page source from
 your browser.
 
-New approach to page tracking in 5.x-1.5 and 6.x-1.1
-====================================================
-With 5.x-1.5 and 6.x-1.1 there are new settings on the settings page at
-admin/config/system/googleanalytics. The "Page specific tracking" area now
-comes with an interface that copies Drupal's block visibility settings.
-
+Page specific tracking
+======================
 The default is set to "Add to every page except the listed pages". By
 default the following pages are listed for exclusion:
 
@@ -44,33 +48,57 @@ node/*/*
 user/*/*
 
 These defaults are changeable by the website administrator or any other
-user with 'administer google analytics' permission.
+user with 'Administer Google Analytics' permission.
 
-Like the blocks visibility settings in Drupal core, there is now a
-choice for "Add if the following PHP code returns TRUE." Sample PHP snippets
-that can be used in this textarea can be found on the handbook page
-"Overview-approach to block visibility" at http://drupal.org/node/64135.
+Like the blocks visibility settings in Drupal core, there is a choice for
+"Add if the following PHP code returns TRUE." Sample PHP snippets that can be
+used in this textarea can be found on the handbook page "Overview-approach to
+block visibility" at https://drupal.org/node/64135.
 
-Custom variables
-=================
-One example for custom variables tracking is the "User roles" tracking. Enter
-the below configuration data into the custom variables settings form under
-admin/config/system/googleanalytics.
+Custom dimensions and metrics
+=============================
+One example for custom dimensions tracking is the "User roles" tracking.
 
-Slot: 1
-Name: User roles
-Value: [current-user:role-names]
-Scope: Visitor
+1. In the Google Analytics (https://marketingplatform.google.com/about/analytics/)
+   Management Interface you need to setup Dimension #1 with name 
+   e.g. "User roles". This step is required. Do not miss it, please.
 
-More details about Custom variables can be found in the Google API documentation at
-http://code.google.com/intl/en/apis/analytics/docs/tracking/gaTrackingCustomVariables.html
+2. Enter the below configuration data into the Drupal custom dimensions settings
+   form under admin/config/system/googleanalytics. You can also choose another
+   index, but keep it always in sync with the index used in step #1.
+
+   Index: 1
+   Value: [current-user:role-names]
+
+More details about custom dimensions and metrics can be found in the Google API
+documentation at https://developers.google.com/analytics/devguides/collection/analyticsjs/custom-dims-mets
 
 Advanced Settings
 =================
 You can include additional JavaScript snippets in the custom javascript
 code textarea. These can be found on the official Google Analytics pages
-and a few examples at http://drupal.org/node/248699. Support is not
+and a few examples at https://drupal.org/node/248699. Support is not
 provided for any customisations you include.
 
-To speed up page loading you may also cache the Analytics ga.js
+To speed up page loading you may also cache the Google Analytics "analytics.js"
 file locally.
+
+Manual JS debugging
+===================
+For manual debugging of the JS code you are able to create a test node. This
+is the example HTML code for this test node. You need to enable debugging mode
+in your Drupal configuration of Google Analytics settings to see verbose
+messages in your browsers JS console.
+
+Title: Google Analytics test page
+
+Body:
+<ul>
+  <li><a href="mailto:foo@example.com">Mailto</a></li>
+  <li><a href="/files/test.txt">Download file</a></li>
+  <li><a class="colorbox" href="#">Open colorbox</a></li>
+  <li><a href="https://example.com/">External link</a></li>
+  <li><a href="/go/test">Go link</a></li>
+</ul>
+
+Text format: Full HTML

File diff suppressed because it is too large
+ 1 - 1
sites/all/modules/contrib/admin/google_analytics/googleanalytics.admin.inc


+ 10 - 1
sites/all/modules/contrib/admin/google_analytics/googleanalytics.admin.js

@@ -65,6 +65,15 @@ Drupal.behaviors.trackingSettingsSummary = {
       if ($('input#edit-googleanalytics-trackfiles', context).is(':checked')) {
         vals.push(Drupal.t('Downloads'));
       }
+      if ($('input#edit-googleanalytics-trackcolorbox', context).is(':checked')) {
+        vals.push(Drupal.t('Colorbox'));
+      }
+      if ($('input#edit-googleanalytics-tracklinkid', context).is(':checked')) {
+        vals.push(Drupal.t('Link attribution'));
+      }
+      if ($('input#edit-googleanalytics-trackurlfragments', context).is(':checked')) {
+        vals.push(Drupal.t('URL fragments'));
+      }
       if (!vals.length) {
         return Drupal.t('Not tracked');
       }
@@ -91,7 +100,7 @@ Drupal.behaviors.trackingSettingsSummary = {
         vals.push(Drupal.t('AdSense ads'));
       }
       if ($('input#edit-googleanalytics-trackdoubleclick', context).is(':checked')) {
-        vals.push(Drupal.t('DoubleClick data'));
+        vals.push(Drupal.t('Display features'));
       }
       if (!vals.length) {
         return Drupal.t('Not tracked');

+ 213 - 0
sites/all/modules/contrib/admin/google_analytics/googleanalytics.debug.js

@@ -0,0 +1,213 @@
+(function ($) {
+
+Drupal.googleanalytics = {};
+
+$(document).ready(function() {
+
+  // Attach mousedown, keyup, touchstart events to document only and catch
+  // clicks on all elements.
+  $(document.body).bind("mousedown keyup touchstart", function(event) {
+    console.group("Running Google Analytics for Drupal.");
+    console.info("Event '%s' has been detected.", event.type);
+
+    // Catch the closest surrounding link of a clicked element.
+    $(event.target).closest("a,area").each(function() {
+      console.info("Closest element '%o' has been found. URL '%s' extracted.", this, this.href);
+
+      // Is the clicked URL internal?
+      if (Drupal.googleanalytics.isInternal(this.href)) {
+        // Skip 'click' tracking, if custom tracking events are bound.
+        if ($(this).is('.colorbox') && (Drupal.settings.googleanalytics.trackColorbox)) {
+          // Do nothing here. The custom event will handle all tracking.
+          console.info("Click on .colorbox item has been detected.");
+        }
+        // Is download tracking activated and the file extension configured for download tracking?
+        else if (Drupal.settings.googleanalytics.trackDownload && Drupal.googleanalytics.isDownload(this.href)) {
+          // Download link clicked.
+          console.info("Download url '%s' has been found. Tracked download as extension '%s'.", Drupal.googleanalytics.getPageUrl(this.href), Drupal.googleanalytics.getDownloadExtension(this.href).toUpperCase());
+          ga("send", {
+            "hitType": "event",
+            "eventCategory": "Downloads",
+            "eventAction": Drupal.googleanalytics.getDownloadExtension(this.href).toUpperCase(),
+            "eventLabel": Drupal.googleanalytics.getPageUrl(this.href),
+            "transport": "beacon"
+          });
+        }
+        else if (Drupal.googleanalytics.isInternalSpecial(this.href)) {
+          // Keep the internal URL for Google Analytics website overlay intact.
+          console.info("Click on internal special link '%s' has been tracked.", Drupal.googleanalytics.getPageUrl(this.href));
+          ga("send", {
+            "hitType": "pageview",
+            "page": Drupal.googleanalytics.getPageUrl(this.href),
+            "transport": "beacon"
+          });
+        }
+        else {
+          // e.g. anchor in same page or other internal page link
+          console.info("Click on internal link '%s' detected, but not tracked by click.", this.href);
+        }
+      }
+      else {
+        if (Drupal.settings.googleanalytics.trackMailto && $(this).is("a[href^='mailto:'],area[href^='mailto:']")) {
+          // Mailto link clicked.
+          console.info("Click on e-mail '%s' has been tracked.", this.href.substring(7));
+          ga("send", {
+            "hitType": "event",
+            "eventCategory": "Mails",
+            "eventAction": "Click",
+            "eventLabel": this.href.substring(7),
+            "transport": "beacon"
+          });
+        }
+        else if (Drupal.settings.googleanalytics.trackOutbound && this.href.match(/^\w+:\/\//i)) {
+          if (Drupal.settings.googleanalytics.trackDomainMode !== 2 || (Drupal.settings.googleanalytics.trackDomainMode === 2 && !Drupal.googleanalytics.isCrossDomain(this.hostname, Drupal.settings.googleanalytics.trackCrossDomains))) {
+            // External link clicked / No top-level cross domain clicked.
+            console.info("Outbound link '%s' has been tracked.", this.href);
+            ga("send", {
+              "hitType": "event",
+              "eventCategory": "Outbound links",
+              "eventAction": "Click",
+              "eventLabel": this.href,
+              "transport": "beacon"
+            });
+          }
+          else {
+            console.info("Internal link '%s' clicked, not tracked.", this.href);
+          }
+        }
+      }
+    });
+
+    console.groupEnd();
+  });
+
+  // Track hash changes as unique pageviews, if this option has been enabled.
+  if (Drupal.settings.googleanalytics.trackUrlFragments) {
+    window.onhashchange = function() {
+      console.info("Track URL '%s' as pageview. Hash '%s' has changed.", location.pathname + location.search + location.hash, location.hash);
+      ga("send", {
+        "hitType": "pageview",
+        "page": location.pathname + location.search + location.hash
+      });
+    };
+  }
+
+  // Colorbox: This event triggers when the transition has completed and the
+  // newly loaded content has been revealed.
+  if (Drupal.settings.googleanalytics.trackColorbox) {
+    $(document).bind("cbox_complete", function () {
+      var href = $.colorbox.element().attr("href");
+      if (href) {
+        console.info("Colorbox transition to url '%s' has been tracked.", Drupal.googleanalytics.getPageUrl(href));
+        ga("send", {
+          "hitType": "pageview",
+          "page": Drupal.googleanalytics.getPageUrl(href)
+        });
+      }
+    });
+  }
+
+});
+
+/**
+ * Check whether the hostname is part of the cross domains or not.
+ *
+ * @param string hostname
+ *   The hostname of the clicked URL.
+ * @param array crossDomains
+ *   All cross domain hostnames as JS array.
+ *
+ * @return boolean
+ */
+Drupal.googleanalytics.isCrossDomain = function (hostname, crossDomains) {
+  /**
+   * jQuery < 1.6.3 bug: $.inArray crushes IE6 and Chrome if second argument is
+   * `null` or `undefined`, https://bugs.jquery.com/ticket/10076,
+   * https://github.com/jquery/jquery/commit/a839af034db2bd934e4d4fa6758a3fed8de74174
+   *
+   * @todo: Remove/Refactor in D8
+   */
+  if (!crossDomains) {
+    return false;
+  }
+  else {
+    return $.inArray(hostname, crossDomains) > -1 ? true : false;
+  }
+};
+
+/**
+ * Check whether this is a download URL or not.
+ *
+ * @param string url
+ *   The web url to check.
+ *
+ * @return boolean
+ */
+Drupal.googleanalytics.isDownload = function (url) {
+  var isDownload = new RegExp("\\.(" + Drupal.settings.googleanalytics.trackDownloadExtensions + ")([\?#].*)?$", "i");
+  return isDownload.test(url);
+};
+
+/**
+ * Check whether this is an absolute internal URL or not.
+ *
+ * @param string url
+ *   The web url to check.
+ *
+ * @return boolean
+ */
+Drupal.googleanalytics.isInternal = function (url) {
+  var isInternal = new RegExp("^(https?):\/\/" + window.location.host, "i");
+  return isInternal.test(url);
+};
+
+/**
+ * Check whether this is a special URL or not.
+ *
+ * URL types:
+ *  - gotwo.module /go/* links.
+ *
+ * @param string url
+ *   The web url to check.
+ *
+ * @return boolean
+ */
+Drupal.googleanalytics.isInternalSpecial = function (url) {
+  var isInternalSpecial = new RegExp("(\/go\/.*)$", "i");
+  return isInternalSpecial.test(url);
+};
+
+/**
+ * Extract the relative internal URL from an absolute internal URL.
+ *
+ * Examples:
+ * - https://mydomain.com/node/1 -> /node/1
+ * - https://example.com/foo/bar -> https://example.com/foo/bar
+ *
+ * @param string url
+ *   The web url to check.
+ *
+ * @return string
+ *   Internal website URL
+ */
+Drupal.googleanalytics.getPageUrl = function (url) {
+  var extractInternalUrl = new RegExp("^(https?):\/\/" + window.location.host, "i");
+  return url.replace(extractInternalUrl, '');
+};
+
+/**
+ * Extract the download file extension from the URL.
+ *
+ * @param string url
+ *   The web url to check.
+ *
+ * @return string
+ *   The file extension of the passed url. e.g. "zip", "txt"
+ */
+Drupal.googleanalytics.getDownloadExtension = function (url) {
+  var extractDownloadextension = new RegExp("\\.(" + Drupal.settings.googleanalytics.trackDownloadExtensions + ")([\?#].*)?$", "i");
+  var extension = extractDownloadextension.exec(url);
+  return (extension === null) ? '' : extension[1];
+};
+
+})(jQuery);

+ 4 - 5
sites/all/modules/contrib/admin/google_analytics/googleanalytics.info

@@ -4,10 +4,9 @@ core = 7.x
 package = Statistics
 configure = admin/config/system/googleanalytics
 files[] = googleanalytics.test
-
-; Information added by drupal.org packaging script on 2012-11-01
-version = "7.x-1.3"
+test_dependencies[] = token
+; Information added by Drupal.org packaging script on 2019-01-31
+version = "7.x-2.6"
 core = "7.x"
 project = "google_analytics"
-datestamp = "1351810914"
-
+datestamp = "1548968597"

File diff suppressed because it is too large
+ 45 - 36
sites/all/modules/contrib/admin/google_analytics/googleanalytics.install


+ 139 - 37
sites/all/modules/contrib/admin/google_analytics/googleanalytics.js

@@ -1,66 +1,93 @@
 (function ($) {
 
+Drupal.googleanalytics = {};
+
 $(document).ready(function() {
 
-  // Expression to check for absolute internal links.
-  var isInternal = new RegExp("^(https?):\/\/" + window.location.host, "i");
+  // Attach mousedown, keyup, touchstart events to document only and catch
+  // clicks on all elements.
+  $(document.body).bind("mousedown keyup touchstart", function(event) {
 
-  // Attach onclick event to document only and catch clicks on all elements.
-  $(document.body).click(function(event) {
     // Catch the closest surrounding link of a clicked element.
     $(event.target).closest("a,area").each(function() {
 
-      var ga = Drupal.settings.googleanalytics;
-      // Expression to check for special links like gotwo.module /go/* links.
-      var isInternalSpecial = new RegExp("(\/go\/.*)$", "i");
-      // Expression to check for download links.
-      var isDownload = new RegExp("\\.(" + ga.trackDownloadExtensions + ")$", "i");
-
       // Is the clicked URL internal?
-      if (isInternal.test(this.href)) {
+      if (Drupal.googleanalytics.isInternal(this.href)) {
         // Skip 'click' tracking, if custom tracking events are bound.
-        if ($(this).is('.colorbox')) {
+        if ($(this).is('.colorbox') && (Drupal.settings.googleanalytics.trackColorbox)) {
           // Do nothing here. The custom event will handle all tracking.
+          //console.info("Click on .colorbox item has been detected.");
         }
         // Is download tracking activated and the file extension configured for download tracking?
-        else if (ga.trackDownload && isDownload.test(this.href)) {
+        else if (Drupal.settings.googleanalytics.trackDownload && Drupal.googleanalytics.isDownload(this.href)) {
           // Download link clicked.
-          var extension = isDownload.exec(this.href);
-          _gaq.push(["_trackEvent", "Downloads", extension[1].toUpperCase(), this.href.replace(isInternal, '')]);
+          ga("send", {
+            "hitType": "event",
+            "eventCategory": "Downloads",
+            "eventAction": Drupal.googleanalytics.getDownloadExtension(this.href).toUpperCase(),
+            "eventLabel": Drupal.googleanalytics.getPageUrl(this.href),
+            "transport": "beacon"
+          });
         }
-        else if (isInternalSpecial.test(this.href)) {
+        else if (Drupal.googleanalytics.isInternalSpecial(this.href)) {
           // Keep the internal URL for Google Analytics website overlay intact.
-          _gaq.push(["_trackPageview", this.href.replace(isInternal, '')]);
+          ga("send", {
+            "hitType": "pageview",
+            "page": Drupal.googleanalytics.getPageUrl(this.href),
+            "transport": "beacon"
+          });
         }
       }
       else {
-        if (ga.trackMailto && $(this).is("a[href^='mailto:'],area[href^='mailto:']")) {
+        if (Drupal.settings.googleanalytics.trackMailto && $(this).is("a[href^='mailto:'],area[href^='mailto:']")) {
           // Mailto link clicked.
-          _gaq.push(["_trackEvent", "Mails", "Click", this.href.substring(7)]);
+          ga("send", {
+            "hitType": "event",
+            "eventCategory": "Mails",
+            "eventAction": "Click",
+            "eventLabel": this.href.substring(7),
+            "transport": "beacon"
+          });
         }
-        else if (ga.trackOutbound && this.href.match(/^\w+:\/\//i)) {
-          if (ga.trackDomainMode == 2 && isCrossDomain($(this).attr('hostname'), ga.trackCrossDomains)) {
-            // Top-level cross domain clicked. document.location is handled by _link internally.
-            event.preventDefault();
-            _gaq.push(["_link", this.href]);
-          }
-          else {
-            // External link clicked.
-            _gaq.push(["_trackEvent", "Outbound links", "Click", this.href]);
+        else if (Drupal.settings.googleanalytics.trackOutbound && this.href.match(/^\w+:\/\//i)) {
+          if (Drupal.settings.googleanalytics.trackDomainMode !== 2 || (Drupal.settings.googleanalytics.trackDomainMode === 2 && !Drupal.googleanalytics.isCrossDomain(this.hostname, Drupal.settings.googleanalytics.trackCrossDomains))) {
+            // External link clicked / No top-level cross domain clicked.
+            ga("send", {
+              "hitType": "event",
+              "eventCategory": "Outbound links",
+              "eventAction": "Click",
+              "eventLabel": this.href,
+              "transport": "beacon"
+            });
           }
         }
       }
     });
   });
 
+  // Track hash changes as unique pageviews, if this option has been enabled.
+  if (Drupal.settings.googleanalytics.trackUrlFragments) {
+    window.onhashchange = function() {
+      ga("send", {
+        "hitType": "pageview",
+        "page": location.pathname + location.search + location.hash
+      });
+    };
+  }
+
   // Colorbox: This event triggers when the transition has completed and the
   // newly loaded content has been revealed.
-  $(document).bind("cbox_complete", function() {
-    var href = $.colorbox.element().attr("href");
-    if (href) {
-      _gaq.push(["_trackPageview", href.replace(isInternal, '')]);
-    }
-  });
+  if (Drupal.settings.googleanalytics.trackColorbox) {
+    $(document).bind("cbox_complete", function () {
+      var href = $.colorbox.element().attr("href");
+      if (href) {
+        ga("send", {
+          "hitType": "pageview",
+          "page": Drupal.googleanalytics.getPageUrl(href)
+        });
+      }
+    });
+  }
 
 });
 
@@ -74,10 +101,10 @@ $(document).ready(function() {
  *
  * @return boolean
  */
-function isCrossDomain(hostname, crossDomains) {
+Drupal.googleanalytics.isCrossDomain = function (hostname, crossDomains) {
   /**
    * jQuery < 1.6.3 bug: $.inArray crushes IE6 and Chrome if second argument is
-   * `null` or `undefined`, http://bugs.jquery.com/ticket/10076,
+   * `null` or `undefined`, https://bugs.jquery.com/ticket/10076,
    * https://github.com/jquery/jquery/commit/a839af034db2bd934e4d4fa6758a3fed8de74174
    *
    * @todo: Remove/Refactor in D8
@@ -88,6 +115,81 @@ function isCrossDomain(hostname, crossDomains) {
   else {
     return $.inArray(hostname, crossDomains) > -1 ? true : false;
   }
-}
+};
+
+/**
+ * Check whether this is a download URL or not.
+ *
+ * @param string url
+ *   The web url to check.
+ *
+ * @return boolean
+ */
+Drupal.googleanalytics.isDownload = function (url) {
+  var isDownload = new RegExp("\\.(" + Drupal.settings.googleanalytics.trackDownloadExtensions + ")([\?#].*)?$", "i");
+  return isDownload.test(url);
+};
+
+/**
+ * Check whether this is an absolute internal URL or not.
+ *
+ * @param string url
+ *   The web url to check.
+ *
+ * @return boolean
+ */
+Drupal.googleanalytics.isInternal = function (url) {
+  var isInternal = new RegExp("^(https?):\/\/" + window.location.host, "i");
+  return isInternal.test(url);
+};
+
+/**
+ * Check whether this is a special URL or not.
+ *
+ * URL types:
+ *  - gotwo.module /go/* links.
+ *
+ * @param string url
+ *   The web url to check.
+ *
+ * @return boolean
+ */
+Drupal.googleanalytics.isInternalSpecial = function (url) {
+  var isInternalSpecial = new RegExp("(\/go\/.*)$", "i");
+  return isInternalSpecial.test(url);
+};
+
+/**
+ * Extract the relative internal URL from an absolute internal URL.
+ *
+ * Examples:
+ * - https://mydomain.com/node/1 -> /node/1
+ * - https://example.com/foo/bar -> https://example.com/foo/bar
+ *
+ * @param string url
+ *   The web url to check.
+ *
+ * @return string
+ *   Internal website URL
+ */
+Drupal.googleanalytics.getPageUrl = function (url) {
+  var extractInternalUrl = new RegExp("^(https?):\/\/" + window.location.host, "i");
+  return url.replace(extractInternalUrl, '');
+};
+
+/**
+ * Extract the download file extension from the URL.
+ *
+ * @param string url
+ *   The web url to check.
+ *
+ * @return string
+ *   The file extension of the passed url. e.g. "zip", "txt"
+ */
+Drupal.googleanalytics.getDownloadExtension = function (url) {
+  var extractDownloadextension = new RegExp("\\.(" + Drupal.settings.googleanalytics.trackDownloadExtensions + ")([\?#].*)?$", "i");
+  var extension = extractDownloadextension.exec(url);
+  return (extension === null) ? '' : extension[1];
+};
 
 })(jQuery);

+ 222 - 127
sites/all/modules/contrib/admin/google_analytics/googleanalytics.module

@@ -1,26 +1,42 @@
 <?php
 
-/*
+/**
  * @file
- * Drupal Module: GoogleAnalytics
- * Adds the required Javascript to the bottom of all your Drupal pages
- * to allow tracking by the Google Analytics statistics package.
+ * Drupal Module: Google Analytics
+ *
+ * Adds the required Javascript to all your Drupal pages to allow tracking by
+ * the Google Analytics statistics package.
  *
- * @author: Alexander Hass <http://drupal.org/user/85918>
+ * @author: Alexander Hass <https://drupal.org/user/85918>
  */
 
-define('GOOGLEANALYTICS_TRACKFILES_EXTENSIONS', '7z|aac|arc|arj|asf|asx|avi|bin|csv|doc|exe|flv|gif|gz|gzip|hqx|jar|jpe?g|js|mp(2|3|4|e?g)|mov(ie)?|msi|msp|pdf|phps|png|ppt|qtm?|ra(m|r)?|sea|sit|tar|tgz|torrent|txt|wav|wma|wmv|wpd|xls|xml|z|zip');
+/**
+ * Define the default file extension list that should be tracked as download.
+ */
+define('GOOGLEANALYTICS_TRACKFILES_EXTENSIONS', '7z|aac|arc|arj|asf|asx|avi|bin|csv|doc(x|m)?|dot(x|m)?|exe|flv|gif|gz|gzip|hqx|jar|jpe?g|js|mp(2|3|4|e?g)|mov(ie)?|msi|msp|pdf|phps|png|ppt(x|m)?|pot(x|m)?|pps(x|m)?|ppam|sld(x|m)?|thmx|qtm?|ra(m|r)?|sea|sit|tar|tgz|torrent|txt|wav|wma|wmv|wpd|xls(x|m|b)?|xlt(x|m)|xlam|xml|z|zip');
 
-// Remove tracking from all administrative pages, see http://drupal.org/node/34970.
+/**
+ * Define default path exclusion list to remove tracking from admin pages,
+ * see https://drupal.org/node/34970 for more information.
+ */
 define('GOOGLEANALYTICS_PAGES', "admin\nadmin/*\nbatch\nnode/add*\nnode/*/*\nuser/*/*");
 
+/**
+ * Advertise the supported google analytics api details.
+ */
+function googleanalytics_api() {
+  return array(
+    'api' => 'analytics.js',
+  );
+}
+
 /**
  * Implements hook_help().
  */
 function googleanalytics_help($path, $arg) {
   switch ($path) {
     case 'admin/config/system/googleanalytics':
-      return t('<a href="@ga_url">Google Analytics</a> is a free (registration required) website traffic and marketing effectiveness service.', array('@ga_url' => 'http://www.google.com/analytics/'));
+      return t('<a href="@ga_url">Google Analytics</a> is a free (registration required) website traffic and marketing effectiveness service.', array('@ga_url' => 'https://marketingplatform.google.com/about/analytics/'));
   }
 }
 
@@ -53,6 +69,11 @@ function googleanalytics_permission() {
       'description' => t('Enter PHP code in the field for tracking visibility settings.'),
       'restrict access' => TRUE,
     ),
+    'add JS snippets for google analytics' => array(
+      'title' => t('Add JavaScript snippets'),
+      'description' => 'Enter JavaScript code snippets for advanced Google Analytics functionality.',
+      'restrict access' => TRUE,
+    ),
   );
 }
 
@@ -77,7 +98,7 @@ function googleanalytics_menu() {
  * Implements hook_page_alter() to insert JavaScript to the appropriate scope/region of the page.
  */
 function googleanalytics_page_alter(&$page) {
-  global $user;
+  global $base_path, $user;
 
   $id = variable_get('googleanalytics_account', '');
 
@@ -88,19 +109,14 @@ function googleanalytics_page_alter(&$page) {
     '404 Not Found',
   );
 
-  // 1. Check if the GA account number has a value.
+  // 1. Check if the GA account number has a valid value.
   // 2. Track page views based on visibility value.
   // 3. Check if we should track the currently active user's role.
   // 4. Ignore pages visibility filter for 404 or 403 status codes.
-  if (!empty($id) && (_googleanalytics_visibility_pages() || in_array($status, $trackable_status_codes)) && _googleanalytics_visibility_user($user)) {
-
-    // We allow different scopes. Default to 'header' but allow user to override if they really need to.
-    $scope = variable_get('googleanalytics_js_scope', 'header');
+  if (preg_match('/^UA-\d+-\d+$/', $id) && (_googleanalytics_visibility_pages() || in_array($status, $trackable_status_codes)) && _googleanalytics_visibility_user($user)) {
 
-    if (variable_get('googleanalytics_trackadsense', FALSE)) {
-      // Custom tracking. Prepend before all other JavaScript.
-      drupal_add_js('window.google_analytics_uacct = ' . drupal_json_encode($id) . ';', array('type' => 'inline', 'group' => JS_LIBRARY-1));
-    }
+    $debug = variable_get('googleanalytics_debug', 0);
+    $url_custom = '';
 
     // Add link tracking.
     $link_settings = array();
@@ -114,16 +130,32 @@ function googleanalytics_page_alter(&$page) {
       $link_settings['trackDownload'] = $track_download;
       $link_settings['trackDownloadExtensions'] = $trackfiles_extensions;
     }
+    if (module_exists('colorbox') && ($track_colorbox = variable_get('googleanalytics_trackcolorbox', 1))) {
+      $link_settings['trackColorbox'] = $track_colorbox;
+    }
     if ($track_domain_mode = variable_get('googleanalytics_domain_mode', 0)) {
-      $link_settings['trackDomainMode'] = $track_domain_mode;
+      $link_settings['trackDomainMode'] = (int) $track_domain_mode;
     }
     if ($track_cross_domains = variable_get('googleanalytics_cross_domains', '')) {
       $link_settings['trackCrossDomains'] = preg_split('/(\r\n?|\n)/', $track_cross_domains);
     }
+    if ($track_url_fragments = variable_get('googleanalytics_trackurlfragments', 0)) {
+      $link_settings['trackUrlFragments'] = $track_url_fragments;
+      $url_custom = 'location.pathname + location.search + location.hash';
+    }
 
     if (!empty($link_settings)) {
       drupal_add_js(array('googleanalytics' => $link_settings), 'setting');
-      drupal_add_js(drupal_get_path('module', 'googleanalytics') . '/googleanalytics.js');
+
+      // Add debugging code.
+      if ($debug) {
+        drupal_add_js(drupal_get_path('module', 'googleanalytics') . '/googleanalytics.debug.js');
+        // Add the JS test in development to the page.
+        //drupal_add_js(drupal_get_path('module', 'googleanalytics') . '/googleanalytics.test.js');
+      }
+      else {
+        drupal_add_js(drupal_get_path('module', 'googleanalytics') . '/googleanalytics.js');
+      }
     }
 
     // Add messages tracking.
@@ -140,15 +172,17 @@ function googleanalytics_page_alter(&$page) {
         // Track only the selected message types.
         if (in_array($type, $message_types)) {
           foreach ($messages as $message) {
-            $message_events .= '_gaq.push(["_trackEvent", ' . drupal_json_encode(t('Messages')) . ', ' . drupal_json_encode($status_heading[$type]) . ', ' . drupal_json_encode(strip_tags($message)) . ']);';
+            // @todo: Track as exceptions?
+            $message_events .= 'ga("send", "event", ' . drupal_json_encode(t('Messages')) . ', ' . drupal_json_encode($status_heading[$type]) . ', ' . drupal_json_encode(strip_tags($message)) . ');';
           }
         }
       }
     }
 
     // Site search tracking support.
-    $url_custom = '';
     if (module_exists('search') && variable_get('googleanalytics_site_search', FALSE) && arg(0) == 'search' && $keys = googleanalytics_search_get_keys()) {
+      // hook_preprocess_search_results() is not executed if search result is
+      // empty. Make sure the counter is set to 0 if there are no results.
       $url_custom = '(window.googleanalytics_search_results) ? ' . drupal_json_encode(url('search/' . arg(1), array('query' => array('search' => $keys)))) . ' : ' . drupal_json_encode(url('search/' . arg(1), array('query' => array('search' => 'no-results:' . $keys, 'cat' => 'no-results'))));
     }
 
@@ -167,77 +201,149 @@ function googleanalytics_page_alter(&$page) {
 
     // Track access denied (403) and file not found (404) pages.
     if ($status == '403 Forbidden') {
-      // See http://www.google.com/support/analytics/bin/answer.py?answer=86927
-      $url_custom = '"/403.html?page=" + document.location.pathname + document.location.search + "&from=" + document.referrer';
+      // See https://www.google.com/support/analytics/bin/answer.py?answer=86927
+      $url_custom = '"' . $base_path . '403.html?page=" + document.location.pathname + document.location.search + "&from=" + document.referrer';
     }
     elseif ($status == '404 Not Found') {
-      $url_custom = '"/404.html?page=" + document.location.pathname + document.location.search + "&from=" + document.referrer';
+      $url_custom = '"' . $base_path . '404.html?page=" + document.location.pathname + document.location.search + "&from=" + document.referrer';
     }
 
-    // Add any custom code snippets if specified.
-    $codesnippet_before = variable_get('googleanalytics_codesnippet_before', '');
-    $codesnippet_after = variable_get('googleanalytics_codesnippet_after', '');
+    // #2693595: User has entered an invalid login and clicked on forgot
+    // password link. This link contains the username or email address and may
+    // get send to Google if we do not override it. Override only if 'name'
+    // query param exists. Last custom url condition, this need to win.
+    //
+    // URLs to protect are:
+    // - user/password?name=username
+    // - user/password?name=foo@example.com
+    if (arg(0) == 'user' && arg(1) == 'password' && array_key_exists('name', drupal_get_query_parameters())) {
+      $url_custom = '"' . $base_path . 'user/password"';
+    }
 
-    // Add custom variables.
-    $googleanalytics_custom_vars = variable_get('googleanalytics_custom_var', array());
+    // Add custom dimensions and metrics.
     $custom_var = '';
-    for ($i = 1; $i < 6; $i++) {
-      $custom_var_name = !empty($googleanalytics_custom_vars['slots'][$i]['name']) ? $googleanalytics_custom_vars['slots'][$i]['name'] : '';
-      if (!empty($custom_var_name)) {
-        $custom_var_value = !empty($googleanalytics_custom_vars['slots'][$i]['value']) ? $googleanalytics_custom_vars['slots'][$i]['value'] : '';
-        $custom_var_scope = !empty($googleanalytics_custom_vars['slots'][$i]['scope']) ? $googleanalytics_custom_vars['slots'][$i]['scope'] : 3;
-
-        $types = array();
-        $node = menu_get_object();
-        if (is_object($node)) {
-          $types += array('node' => $node);
-        }
-        $custom_var_name = token_replace($custom_var_name, $types, array('clear' => TRUE));
-        $custom_var_value = token_replace($custom_var_value, $types, array('clear' => TRUE));
+    foreach (array('dimension', 'metric') as $googleanalytics_custom_type) {
+      $googleanalytics_custom_vars = variable_get('googleanalytics_custom_' . $googleanalytics_custom_type, array());
+      // Are there dimensions or metrics configured?
+      if (!empty($googleanalytics_custom_vars)) {
+        // Add all the configured variables to the content.
+        foreach ($googleanalytics_custom_vars as $googleanalytics_custom_var) {
+          // Replace tokens in values.
+          $types = array();
+          $node = menu_get_object();
+          if (is_object($node)) {
+            $types += array('node' => $node);
+          }
+          $googleanalytics_custom_var['value'] = token_replace($googleanalytics_custom_var['value'], $types, array('clear' => TRUE));
 
-        // Suppress empty custom names and/or variables.
-        if (!drupal_strlen(trim($custom_var_name)) || !drupal_strlen(trim($custom_var_value))) {
-          continue;
-        }
+          // Suppress empty values.
+          if (!drupal_strlen(trim($googleanalytics_custom_var['value']))) {
+            continue;
+          }
 
-        // The length of the string used for the 'name' and the length of the
-        // string used for the 'value' must not exceed 128 bytes after url encoding.
-        $name_length = drupal_strlen(rawurlencode($custom_var_name));
-        $tmp_value = rawurlencode($custom_var_value);
-        $value_length = drupal_strlen($tmp_value);
-        if ($name_length + $value_length > 128) {
-          // Trim value and remove fragments of url encoding.
-          $tmp_value = rtrim(substr($tmp_value, 0, 127 - $name_length), '%0..9A..F');
-          $custom_var_value = urldecode($tmp_value);
-        }
+          // Per documentation the max length of a dimension is 150 bytes.
+          // A metric has no length limitation. It's not documented if this
+          // limit means 150 bytes after url encoding or before.
+          // See https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#customs
+          if ($googleanalytics_custom_type == 'dimension' && drupal_strlen($googleanalytics_custom_var['value']) > 150) {
+            $googleanalytics_custom_var['value'] = substr($googleanalytics_custom_var['value'], 0, 150);
+          }
 
-        $custom_var_name = drupal_json_encode($custom_var_name);
-        $custom_var_value = drupal_json_encode($custom_var_value);
-        $custom_var .= "_gaq.push(['_setCustomVar', $i, $custom_var_name, $custom_var_value, $custom_var_scope]);";
+          // Cast metric values for json_encode to data type numeric.
+          if ($googleanalytics_custom_type == 'metric') {
+            settype($googleanalytics_custom_var['value'], 'float');
+          };
+
+          // Add variables to tracker.
+          $custom_var .= 'ga("set", ' . drupal_json_encode($googleanalytics_custom_type . $googleanalytics_custom_var['index']) . ', ' . drupal_json_encode($googleanalytics_custom_var['value']) . ');';
+        }
       }
     }
 
     // Build tracker code.
-    $script = 'var _gaq = _gaq || [];';
-    $script .= '_gaq.push(["_setAccount", ' . drupal_json_encode($id) . ']);';
-    if (variable_get('googleanalytics_tracker_anonymizeip', 0)) {
-      // FIXME: The Google API is currently broken and "_gat._anonymizeIp" is only
-      // a workaround until "_anonymizeIp" has been implemented/fixed.
-      $script .= '_gaq.push(["_gat._anonymizeIp"]);';
+    $script = '(function(i,s,o,g,r,a,m){';
+    $script .= 'i["GoogleAnalyticsObject"]=r;i[r]=i[r]||function(){';
+    $script .= '(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),';
+    $script .= 'm=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)';
+    $script .= '})(window,document,"script",';
+
+    // Which version of the tracking library should be used?
+    $library_tracker_url = 'https://www.google-analytics.com/' . ($debug ? 'analytics_debug.js' : 'analytics.js');
+
+    // Should a local cached copy of analytics.js be used?
+    if (variable_get('googleanalytics_cache', 0) && $url = _googleanalytics_cache($library_tracker_url)) {
+      // 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');
+
+      $script .= '"' . $url . $query_string . '"';
+    }
+    else {
+      $script .= '"' . $library_tracker_url . '"';
     }
+    $script .= ',"ga");';
+
+    // Add any custom code snippets if specified.
+    $codesnippet_create = variable_get('googleanalytics_codesnippet_create', array());
+    $codesnippet_before = variable_get('googleanalytics_codesnippet_before', '');
+    $codesnippet_after = variable_get('googleanalytics_codesnippet_after', '');
+
+    // Build the create only fields list.
+    $create_only_fields = array('cookieDomain' => 'auto');
+    $create_only_fields = array_merge($create_only_fields, $codesnippet_create);
 
     // Domain tracking type.
     global $cookie_domain;
     $domain_mode = variable_get('googleanalytics_domain_mode', 0);
+    $googleanalytics_adsense_script = '';
 
     // Per RFC 2109, cookie domains must contain at least one dot other than the
     // first. For hosts such as 'localhost' or IP Addresses we don't set a cookie domain.
     if ($domain_mode == 1 && count(explode('.', $cookie_domain)) > 2 && !is_numeric(str_replace('.', '', $cookie_domain))) {
-      $script .= '_gaq.push(["_setDomainName", ' . drupal_json_encode($cookie_domain) . ']);';
+      $create_only_fields = array_merge($create_only_fields, array('cookieDomain' => $cookie_domain));
+      $googleanalytics_adsense_script .= 'window.google_analytics_domain_name = ' . drupal_json_encode($cookie_domain) . ';';
     }
     elseif ($domain_mode == 2) {
-      $script .= '_gaq.push(["_setDomainName", "none"]);';
-      $script .= '_gaq.push(["_setAllowLinker", true]);';
+      // Cross Domain tracking. 'autoLinker' need to be enabled in 'create'.
+      $create_only_fields = array_merge($create_only_fields, array('allowLinker' => TRUE));
+      $googleanalytics_adsense_script .= 'window.google_analytics_domain_name = "none";';
+    }
+
+    // Track logged in users across all devices.
+    if (variable_get('googleanalytics_trackuserid', 0) && user_is_logged_in()) {
+      $create_only_fields['userId'] = google_analytics_user_id_hash($user->uid);
+    }
+
+    // Create a tracker.
+    $script .= 'ga("create", ' . drupal_json_encode($id) . ', ' . drupal_json_encode($create_only_fields) .');';
+
+    // Prepare Adsense tracking.
+    $googleanalytics_adsense_script .= 'window.google_analytics_uacct = ' . drupal_json_encode($id) . ';';
+
+    // Add enhanced link attribution after 'create', but before 'pageview' send.
+    // @see https://support.google.com/analytics/answer/2558867
+    if (variable_get('googleanalytics_tracklinkid', 0)) {
+      $script .= 'ga("require", "linkid", "linkid.js");';
+    }
+
+    // Add display features after 'create', but before 'pageview' send.
+    // @see https://support.google.com/analytics/answer/2444872
+    if (variable_get('googleanalytics_trackdoubleclick', 0)) {
+      $script .= 'ga("require", "displayfeatures");';
+    }
+
+    // Domain tracking type.
+    if ($domain_mode == 2) {
+      // Cross Domain tracking
+      // https://developers.google.com/analytics/devguides/collection/upgrade/reference/gajs-analyticsjs#cross-domain
+      $script .= 'ga("require", "linker");';
+      $script .= 'ga("linker:autoLink", ' . drupal_json_encode($link_settings['trackCrossDomains']) . ');';
+    }
+
+    if (variable_get('googleanalytics_tracker_anonymizeip', 1)) {
+      $script .= 'ga("set", "anonymizeIp", true);';
     }
 
     if (!empty($custom_var)) {
@@ -246,12 +352,11 @@ function googleanalytics_page_alter(&$page) {
     if (!empty($codesnippet_before)) {
       $script .= $codesnippet_before;
     }
-    if (empty($url_custom)) {
-      $script .= '_gaq.push(["_trackPageview"]);';
-    }
-    else {
-      $script .= '_gaq.push(["_trackPageview", ' . $url_custom . ']);';
+    if (!empty($url_custom)) {
+      $script .= 'ga("set", "page", ' . $url_custom . ');';
     }
+    $script .= 'ga("send", "pageview");';
+
     if (!empty($message_events)) {
       $script .= $message_events;
     }
@@ -259,48 +364,34 @@ function googleanalytics_page_alter(&$page) {
       $script .= $codesnippet_after;
     }
 
-    $script .= '(function() {';
-    $script .= 'var ga = document.createElement("script");';
-    $script .= 'ga.type = "text/javascript";';
-    $script .= 'ga.async = true;';
-
-    // Which version of the tracking library should be used?
-    if ($trackdoubleclick = variable_get('googleanalytics_trackdoubleclick', FALSE)) {
-      $library_tracker_url = 'stats.g.doubleclick.net/dc.js';
-      $library_cache_url = 'http://' . $library_tracker_url;
-    }
-    else {
-      $library_tracker_url = '.google-analytics.com/ga.js';
-      $library_cache_url = 'http://www' . $library_tracker_url;
-    }
-
-    // Should a local cached copy of ga.js be used?
-    if (variable_get('googleanalytics_cache', 0) && $url = _googleanalytics_cache($library_cache_url)) {
-      // 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');
-
-      $script .= 'ga.src = "' . $url . $query_string . '";';
-    }
-    else {
-      // Library paths do not follow the same naming convention.
-      if ($trackdoubleclick) {
-        $script .= 'ga.src = ("https:" == document.location.protocol ? "https://" : "http://") + "' . $library_tracker_url . '";';
-      }
-      else {
-        $script .= 'ga.src = ("https:" == document.location.protocol ? "https://ssl" : "http://www") + "' . $library_tracker_url . '";';
-      }
+    if (variable_get('googleanalytics_trackadsense', FALSE)) {
+      // Custom tracking. Prepend before all other JavaScript.
+      // @TODO: https://support.google.com/adsense/answer/98142
+      // sounds like it could be appended to $script.
+      drupal_add_js($googleanalytics_adsense_script, array('type' => 'inline', 'group' => JS_LIBRARY-1, 'requires_jquery' => FALSE));
     }
-    $script .= 'var s = document.getElementsByTagName("script")[0];';
-    $script .= 's.parentNode.insertBefore(ga, s);';
-    $script .= '})();';
 
-    drupal_add_js($script, array('scope' => $scope, 'type' => 'inline'));
+    drupal_add_js($script, array('scope' => 'header', 'type' => 'inline', 'requires_jquery' => FALSE));
   }
 }
 
+/**
+ * Generate user id hash to implement USER_ID.
+ *
+ * The USER_ID value should be a unique, persistent, and non-personally
+ * identifiable string identifier that represents a user or signed-in
+ * account across devices.
+ *
+ * @param int $uid
+ *   User id.
+ *
+ * @return string
+ *   User id hash.
+ */
+function google_analytics_user_id_hash($uid) {
+  return drupal_hmac_base64($uid, drupal_get_private_key() . drupal_get_hash_salt());
+}
+
 /**
  * Implements hook_field_extra_fields().
  */
@@ -382,13 +473,7 @@ function googleanalytics_user_presave(&$edit, $account, $category) {
 function googleanalytics_cron() {
   // Regenerate the tracking code file every day.
   if (REQUEST_TIME - variable_get('googleanalytics_last_cache', 0) >= 86400 && variable_get('googleanalytics_cache', 0)) {
-    // Which version of the tracking library should be used?
-    if (variable_get('googleanalytics_trackdoubleclick', FALSE)) {
-      _googleanalytics_cache('http://stats.g.doubleclick.net/dc.js', TRUE);
-    }
-    else {
-      _googleanalytics_cache('http://www.google-analytics.com/ga.js', TRUE);
-    }
+    _googleanalytics_cache('https://www.google-analytics.com/analytics.js', TRUE);
     variable_set('googleanalytics_last_cache', REQUEST_TIME);
   }
 }
@@ -399,17 +484,19 @@ function googleanalytics_cron() {
  * Collects and adds the number of search results to the head.
  */
 function googleanalytics_preprocess_search_results(&$variables) {
-  // There is no search result $variable available that hold the number of items
-  // found. But the pager item mumber can tell the number of search results.
-  global $pager_total_items;
+  if (variable_get('googleanalytics_site_search', FALSE)) {
+    // There is no search result $variable available that hold the number of items
+    // found. But the pager item mumber can tell the number of search results.
+    global $pager_total_items;
 
-  drupal_add_js('window.googleanalytics_search_results = ' . intval($pager_total_items[0]) . ';', array('type' => 'inline', 'group' => JS_LIBRARY-1));
+    drupal_add_js('window.googleanalytics_search_results = ' . intval($pager_total_items[0]) . ';', array('type' => 'inline', 'group' => JS_LIBRARY-1, 'requires_jquery' => FALSE));
+  }
 }
 
 /**
  * Helper function for grabbing search keys. Function is missing in D7.
  *
- * http://api.drupal.org/api/function/search_get_keys/6
+ * https://api.drupal.org/api/function/search_get_keys/6
  */
 function googleanalytics_search_get_keys() {
   static $return;
@@ -428,16 +515,16 @@ function googleanalytics_search_get_keys() {
  *
  * @param $location
  *   The full URL to the external javascript file.
- * @param $sync_cached_file
- *   Synchronize tracking code and update if remote file have changed.
+ * @param $synchronize
+ *   Synchronize to local cache if remote file has changed.
  * @return mixed
  *   The path to the local javascript file on success, boolean FALSE on failure.
  */
-function _googleanalytics_cache($location, $sync_cached_file = FALSE) {
+function _googleanalytics_cache($location, $synchronize = FALSE) {
   $path = 'public://googleanalytics';
   $file_destination = $path . '/' . basename($location);
 
-  if (!file_exists($file_destination) || $sync_cached_file) {
+  if (!file_exists($file_destination) || $synchronize) {
     // Download the latest tracking code.
     $result = drupal_http_request($location);
 
@@ -450,6 +537,10 @@ function _googleanalytics_cache($location, $sync_cached_file = FALSE) {
         if ($data_hash_local != $data_hash_remote && file_prepare_directory($path)) {
           // Save updated tracking code file to disk.
           file_unmanaged_save_data($result->data, $file_destination, FILE_EXISTS_REPLACE);
+          // Based on Drupal Core drupal_build_css_cache().
+          if (variable_get('css_gzip_compression', TRUE) && variable_get('clean_url', 0) && extension_loaded('zlib')) {
+            file_unmanaged_save_data(gzencode($result->data, 9, FORCE_GZIP), $file_destination . '.gz', FILE_EXISTS_REPLACE);
+          }
           watchdog('googleanalytics', 'Locally cached tracking code file has been updated.', array(), WATCHDOG_INFO);
 
           // Change query-strings on css/js files to enforce reload for all users.
@@ -462,6 +553,10 @@ function _googleanalytics_cache($location, $sync_cached_file = FALSE) {
           // There is no need to flush JS here as core refreshes JS caches
           // automatically, if new files are added.
           file_unmanaged_save_data($result->data, $file_destination, FILE_EXISTS_REPLACE);
+          // Based on Drupal Core drupal_build_css_cache().
+          if (variable_get('css_gzip_compression', TRUE) && variable_get('clean_url', 0) && extension_loaded('zlib')) {
+            file_unmanaged_save_data(gzencode($result->data, 9, FORCE_GZIP), $file_destination . '.gz', FILE_EXISTS_REPLACE);
+          }
           watchdog('googleanalytics', 'Locally cached tracking code file has been saved.', array(), WATCHDOG_INFO);
 
           // Return the local JS file path.

+ 512 - 135
sites/all/modules/contrib/admin/google_analytics/googleanalytics.test

@@ -6,10 +6,17 @@
  */
 class GoogleAnalyticsBasicTest extends DrupalWebTestCase {
 
+  /**
+   * User without permissions to edit snippets.
+   *
+   * @var \StdClass
+   */
+  protected $noSnippetUser;
+
   public static function getInfo() {
     return array(
-      'name' => t('Google Analytics basic tests'),
-      'description' => t('Test basic functionality of Google Analytics module.'),
+      'name' => 'Google Analytics basic tests',
+      'description' => 'Test basic functionality of Google Analytics module.',
       'group' => 'Google Analytics',
     );
   }
@@ -20,31 +27,70 @@ class GoogleAnalyticsBasicTest extends DrupalWebTestCase {
     $permissions = array(
       'access administration pages',
       'administer google analytics',
+      'administer modules',
+      'administer site configuration',
     );
 
     // User to set up google_analytics.
+    $this->noSnippetUser = $this->drupalCreateUser($permissions);
+    $permissions[] = 'add JS snippets for google analytics';
     $this->admin_user = $this->drupalCreateUser($permissions);
     $this->drupalLogin($this->admin_user);
   }
 
   function testGoogleAnalyticsConfiguration() {
+    // Check if Configure link is available on 'Modules' page.
+    // Requires 'administer modules' permission.
+    $this->drupalGet('admin/modules');
+    $this->assertRaw('admin/config/system/googleanalytics', '[testGoogleAnalyticsConfiguration]: Configure link from Modules page to Google Analytics Settings page exists.');
+
+    // Check if Configure link is available on 'Status Reports' page. NOTE: Link is only shown without UA code configured.
+    // Requires 'administer site configuration' permission.
+    $this->drupalGet('admin/reports/status');
+    $this->assertRaw('admin/config/system/googleanalytics', '[testGoogleAnalyticsConfiguration]: Configure link from Status Reports page to Google Analytics Settings page exists.');
+
     // Check for setting page's presence.
     $this->drupalGet('admin/config/system/googleanalytics');
     $this->assertRaw(t('Web Property ID'), '[testGoogleAnalyticsConfiguration]: Settings page displayed.');
 
     // Check for account code validation.
     $edit['googleanalytics_account'] = $this->randomName(2);
-    $this->drupalPost('admin/config/system/googleanalytics', $edit, 'Save configuration');
+    $this->drupalPost('admin/config/system/googleanalytics', $edit, t('Save configuration'));
     $this->assertRaw(t('A valid Google Analytics Web Property ID is case sensitive and formatted like UA-xxxxxxx-yy.'), '[testGoogleAnalyticsConfiguration]: Invalid Web Property ID number validated.');
+
+    // User should have access to code snippets.
+    $this->assertFieldByName('googleanalytics_codesnippet_create');
+    $this->assertFieldByName('googleanalytics_codesnippet_before');
+    $this->assertFieldByName('googleanalytics_codesnippet_after');
+    $this->assertNoFieldByXPath("//textarea[@name='googleanalytics_codesnippet_create' and @disabled='disabled']", NULL, '"Create only fields" is enabled.');
+    $this->assertNoFieldByXPath("//textarea[@name='googleanalytics_codesnippet_before' and @disabled='disabled']", NULL, '"Code snippet (before)" is enabled.');
+    $this->assertNoFieldByXPath("//textarea[@name='googleanalytics_codesnippet_after' and @disabled='disabled']", NULL, '"Code snippet (after)" is enabled.');
+
+    // Login as user without JS permissions.
+    $this->drupalLogin($this->noSnippetUser);
+    $this->drupalGet('admin/config/system/googleanalytics');
+
+    // User should *not* have access to snippets, but create fields.
+    $this->assertFieldByName('googleanalytics_codesnippet_create');
+    $this->assertFieldByName('googleanalytics_codesnippet_before');
+    $this->assertFieldByName('googleanalytics_codesnippet_after');
+    $this->assertNoFieldByXPath("//textarea[@name='googleanalytics_codesnippet_create' and @disabled='disabled']", NULL, '"Create only fields" is enabled.');
+    $this->assertFieldByXPath("//textarea[@name='googleanalytics_codesnippet_before' and @disabled='disabled']", NULL, '"Code snippet (before)" is disabled.');
+    $this->assertFieldByXPath("//textarea[@name='googleanalytics_codesnippet_after' and @disabled='disabled']", NULL, '"Code snippet (after)" is disabled.');
   }
 
   function testGoogleAnalyticsPageVisibility() {
+    // Verify that no tracking code is embedded into the webpage; if there is
+    // only the module installed, but UA code not configured. See #2246991.
+    $this->drupalGet('');
+    $this->assertNoRaw('https://www.google-analytics.com/analytics.js', '[testGoogleAnalyticsPageVisibility]: Tracking code is not displayed without UA code configured.');
+
     $ua_code = 'UA-123456-1';
     variable_set('googleanalytics_account', $ua_code);
 
     // Show tracking on "every page except the listed pages".
     variable_set('googleanalytics_visibility_pages', 0);
-    // Disable tracking one "admin*" pages only.
+    // Disable tracking on "admin*" pages only.
     variable_set('googleanalytics_pages', "admin\nadmin/*");
     // Enable tracking only for authenticated users only.
     variable_set('googleanalytics_roles', array(DRUPAL_AUTHENTICATED_RID => DRUPAL_AUTHENTICATED_RID));
@@ -58,7 +104,7 @@ class GoogleAnalyticsBasicTest extends DrupalWebTestCase {
     $this->assertNoRaw($ua_code, '[testGoogleAnalyticsPageVisibility]: Tracking code is not displayed on admin page.');
     $this->drupalGet('admin/config/system/googleanalytics');
     // Checking for tracking code URI here, as $ua_code is displayed in the form.
-    $this->assertNoRaw('google-analytics.com/ga.js', '[testGoogleAnalyticsPageVisibility]: Tracking code is not displayed on admin subpage.');
+    $this->assertNoRaw('https://www.google-analytics.com/analytics.js', '[testGoogleAnalyticsPageVisibility]: Tracking code is not displayed on admin subpage.');
 
     // Test whether tracking code display is properly flipped.
     variable_set('googleanalytics_visibility_pages', 1);
@@ -66,7 +112,7 @@ class GoogleAnalyticsBasicTest extends DrupalWebTestCase {
     $this->assertRaw($ua_code, '[testGoogleAnalyticsPageVisibility]: Tracking code is displayed on admin page.');
     $this->drupalGet('admin/config/system/googleanalytics');
     // Checking for tracking code URI here, as $ua_code is displayed in the form.
-    $this->assertRaw('google-analytics.com/ga.js', '[testGoogleAnalyticsPageVisibility]: Tracking code is displayed on admin subpage.');
+    $this->assertRaw('https://www.google-analytics.com/analytics.js', '[testGoogleAnalyticsPageVisibility]: Tracking code is displayed on admin subpage.');
     $this->drupalGet('');
     $this->assertNoRaw($ua_code, '[testGoogleAnalyticsPageVisibility]: Tracking code is NOT displayed on front page.');
 
@@ -80,31 +126,33 @@ class GoogleAnalyticsBasicTest extends DrupalWebTestCase {
     // Enable tracking code for all user roles.
     variable_set('googleanalytics_roles', array());
 
+    $base_path = base_path();
+
     // Test whether 403 forbidden tracking code is shown if user has no access.
     $this->drupalGet('admin');
-    $this->assertRaw('/403.html', '[testGoogleAnalyticsPageVisibility]: 403 Forbidden tracking code shown if user has no access.');
+    $this->assertRaw($base_path . '403.html', '[testGoogleAnalyticsPageVisibility]: 403 Forbidden tracking code shown if user has no access.');
 
     // Test whether 404 not found tracking code is shown on non-existent pages.
     $this->drupalGet($this->randomName(64));
-    $this->assertRaw('/404.html', '[testGoogleAnalyticsPageVisibility]: 404 Not Found tracking code shown on non-existent page.');
+    $this->assertRaw($base_path . '404.html', '[testGoogleAnalyticsPageVisibility]: 404 Not Found tracking code shown on non-existent page.');
 
     // DNT Tests:
-    // Enable caching of pages for anonymous users.
+    // Enable system internal page cache for anonymous users.
     variable_set('cache', 1);
     // Test whether DNT headers will fail to disable embedding of tracking code.
     $this->drupalGet('', array(), array('DNT: 1'));
-    $this->assertRaw('_gaq.push(["_trackPageview"]);', '[testGoogleAnalyticsDNTVisibility]: DNT header send from client, but page caching is enabled and tracker cannot removed.');
-    // DNT works only with caching of pages for anonymous users disabled.
+    $this->assertRaw('ga("send", "pageview");', '[testGoogleAnalyticsDNTVisibility]: DNT header send from client, but page caching is enabled and tracker cannot removed.');
+    // DNT works only with system internal page cache for anonymous users disabled.
     variable_set('cache', 0);
     $this->drupalGet('');
-    $this->assertRaw('_gaq.push(["_trackPageview"]);', '[testGoogleAnalyticsDNTVisibility]: Tracking is enabled without DNT header.');
+    $this->assertRaw('ga("send", "pageview");', '[testGoogleAnalyticsDNTVisibility]: Tracking is enabled without DNT header.');
     // Test whether DNT header is able to remove the tracking code.
     $this->drupalGet('', array(), array('DNT: 1'));
-    $this->assertNoRaw('_gaq.push(["_trackPageview"]);', '[testGoogleAnalyticsDNTVisibility]: DNT header received from client. Tracking has been disabled by browser.');
+    $this->assertNoRaw('ga("send", "pageview");', '[testGoogleAnalyticsDNTVisibility]: DNT header received from client. Tracking has been disabled by browser.');
     // Disable DNT feature and see if tracker is still embedded.
     variable_set('googleanalytics_privacy_donottrack', 0);
     $this->drupalGet('', array(), array('DNT: 1'));
-    $this->assertRaw('_gaq.push(["_trackPageview"]);', '[testGoogleAnalyticsDNTVisibility]: DNT feature is disabled, DNT header from browser has been ignored.');
+    $this->assertRaw('ga("send", "pageview");', '[testGoogleAnalyticsDNTVisibility]: DNT feature is disabled, DNT header from browser has been ignored.');
   }
 
   function testGoogleAnalyticsTrackingCode() {
@@ -118,40 +166,75 @@ class GoogleAnalyticsBasicTest extends DrupalWebTestCase {
 
     /* Sample JS code as added to page:
     <script type="text/javascript" src="/sites/all/modules/google_analytics/googleanalytics.js?w"></script>
-    <script type="text/javascript">
-      var _gaq = _gaq || [];
-      _gaq.push(['_setAccount', 'UA-123456-7']);
-      _gaq.push(['_trackPageview']);
-
-      (function() {
-        var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
-        ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
-        var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
-      })();
+    <script>
+    (function(q,u,i,c,k){window['GoogleAnalyticsObject']=q;
+    window[q]=window[q]||function(){(window[q].q=window[q].q||[]).push(arguments)},
+    window[q].l=1*new Date();c=i.createElement(u),k=i.getElementsByTagName(u)[0];
+    c.async=true;c.src='https://www.google-analytics.com/analytics.js';
+    k.parentNode.insertBefore(c,k)})('ga','script',document);
+    ga('create', 'UA-123456-7');
+    ga('send', 'pageview');
     </script>
+    <!-- End Google Analytics -->
     */
 
     // Test whether tracking code uses latest JS.
     variable_set('googleanalytics_cache', 0);
     $this->drupalGet('');
-    $this->assertRaw('google-analytics.com/ga.js', '[testGoogleAnalyticsTrackingCode]: Latest tracking code used.');
-
-    // Test whether the alternate doubleclick library is used
-    variable_set('googleanalytics_trackdoubleclick', 1);
-    $this->drupalGet('');
-    $this->assertRaw('stats.g.doubleclick.net/dc.js', '[testGoogleAnalyticsTrackingCode]: Doubleclick tracking code used.');
+    $this->assertRaw('https://www.google-analytics.com/analytics.js', '[testGoogleAnalyticsTrackingCode]: Latest tracking code used.');
 
     // Test whether anonymize visitors IP address feature has been enabled.
+    variable_set('googleanalytics_tracker_anonymizeip', 0);
     $this->drupalGet('');
-    $this->assertNoRaw('_gaq.push(["_gat._anonymizeIp"]);', '[testGoogleAnalyticsTrackingCode]: Anonymize visitors IP address not found on frontpage.');
+    $this->assertNoRaw('ga("set", "anonymizeIp", true);', '[testGoogleAnalyticsTrackingCode]: Anonymize visitors IP address not found on frontpage.');
     // Enable anonymizing of IP addresses.
     variable_set('googleanalytics_tracker_anonymizeip', 1);
     $this->drupalGet('');
-    $this->assertRaw('_gaq.push(["_gat._anonymizeIp"]);', '[testGoogleAnalyticsTrackingCode]: Anonymize visitors IP address found on frontpage.');
+    $this->assertRaw('ga("set", "anonymizeIp", true);', '[testGoogleAnalyticsTrackingCode]: Anonymize visitors IP address found on frontpage.');
+
+    // Test if track Enhanced Link Attribution is enabled.
+    variable_set('googleanalytics_tracklinkid', 1);
+    $this->drupalGet('');
+    $this->assertRaw('ga("require", "linkid", "linkid.js");', '[testGoogleAnalyticsTrackingCode]: Tracking code for Enhanced Link Attribution is enabled.');
+
+    // Test if track Enhanced Link Attribution is disabled.
+    variable_set('googleanalytics_tracklinkid', 0);
+    $this->drupalGet('');
+    $this->assertNoRaw('ga("require", "linkid", "linkid.js");', '[testGoogleAnalyticsTrackingCode]: Tracking code for Enhanced Link Attribution is not enabled.');
+
+    // Test if tracking of User ID is enabled.
+    variable_set('googleanalytics_trackuserid', 1);
+    $this->drupalGet('');
+    $this->assertRaw(', {"cookieDomain":"auto","userId":"', '[testGoogleAnalyticsTrackingCode]: Tracking code for User ID is enabled.');
+
+    // Test if tracking of User ID is disabled.
+    variable_set('googleanalytics_trackuserid', 0);
+    $this->drupalGet('');
+    $this->assertNoRaw(', {"cookieDomain":"auto","userId":"', '[testGoogleAnalyticsTrackingCode]: Tracking code for User ID is disabled.');
+
+    // Test if tracking of url fragments is enabled.
+    variable_set('googleanalytics_trackurlfragments', 1);
+    $this->drupalGet('');
+    $this->assertRaw('ga("set", "page", location.pathname + location.search + location.hash);', '[testGoogleAnalyticsTrackingCode]: Tracking code for url fragments is enabled.');
+
+    // Test if tracking of url fragments is disabled.
+    variable_set('googleanalytics_trackurlfragments', 0);
+    $this->drupalGet('');
+    $this->assertNoRaw('ga("set", "page", location.pathname + location.search + location.hash);', '[testGoogleAnalyticsTrackingCode]: Tracking code for url fragments is not enabled.');
+
+    // Test if track display features is enabled.
+    variable_set('googleanalytics_trackdoubleclick', 1);
+    $this->drupalGet('');
+    $this->assertRaw('ga("require", "displayfeatures");', '[testGoogleAnalyticsTrackingCode]: Tracking code for display features is enabled.');
+
+    // Test if track display features is disabled.
+    variable_set('googleanalytics_trackdoubleclick', 0);
+    $this->drupalGet('');
+    $this->assertNoRaw('ga("require", "displayfeatures");', '[testGoogleAnalyticsTrackingCode]: Tracking code for display features is not enabled.');
 
     // Test whether single domain tracking is active.
     $this->drupalGet('');
-    $this->assertNoRaw('_gaq.push(["_setDomainName"', '[testGoogleAnalyticsTrackingCode]: Single domain tracking is active.');
+    $this->assertRaw('{"cookieDomain":"auto"}', '[testGoogleAnalyticsTrackingCode]: Single domain tracking is active.');
 
     // Enable "One domain with multiple subdomains".
     variable_set('googleanalytics_domain_mode', 1);
@@ -161,36 +244,63 @@ class GoogleAnalyticsBasicTest extends DrupalWebTestCase {
     // TODO: Workaround to run tests successfully. This feature cannot tested reliable.
     global $cookie_domain;
     if (count(explode('.', $cookie_domain)) > 2 && !is_numeric(str_replace('.', '', $cookie_domain))) {
-      $this->assertRaw('_gaq.push(["_setDomainName",', '[testGoogleAnalyticsTrackingCode]: One domain with multiple subdomains is active on real host.');
+      $this->assertRaw('{"cookieDomain":"' . $cookie_domain . '"}', '[testGoogleAnalyticsTrackingCode]: One domain with multiple subdomains is active on real host.');
     }
     else {
       // Special cases, Localhost and IP addresses don't show '_setDomainName'.
-      $this->assertNoRaw('_gaq.push(["_setDomainName",', '[testGoogleAnalyticsTrackingCode]: One domain with multiple subdomains may be active on localhost (test result is not reliable).');
+      $this->assertNoRaw('{"cookieDomain":"' . $cookie_domain . '"}', '[testGoogleAnalyticsTrackingCode]: One domain with multiple subdomains may be active on localhost (test result is not reliable).');
     }
 
     // Enable "Multiple top-level domains" tracking.
     variable_set('googleanalytics_domain_mode', 2);
     variable_set('googleanalytics_cross_domains', "www.example.com\nwww.example.net");
     $this->drupalGet('');
-    $this->assertRaw('_gaq.push(["_setDomainName", "none"]);', '[testGoogleAnalyticsTrackingCode]: _setDomainName: "none" found. Cross domain tracking is active.');
-    $this->assertRaw('_gaq.push(["_setAllowLinker", true]);', '[testGoogleAnalyticsTrackingCode]: _setAllowLinker: true found. Cross domain tracking is active.');
+    $this->assertRaw('ga("create", "' . $ua_code . '", {"cookieDomain":"auto","allowLinker":true', '[testGoogleAnalyticsTrackingCode]: "allowLinker" has been found. Cross domain tracking is active.');
+    $this->assertRaw('ga("require", "linker");', '[testGoogleAnalyticsTrackingCode]: Require linker has been found. Cross domain tracking is active.');
+    $this->assertRaw('ga("linker:autoLink", ["www.example.com","www.example.net"]);', '[testGoogleAnalyticsTrackingCode]: "linker:autoLink" has been found. Cross domain tracking is active.');
+    $this->assertRaw('"trackDomainMode":2,', '[testGoogleAnalyticsTrackingCode]: Domain mode value is of type integer.');
     $this->assertRaw('"trackCrossDomains":["www.example.com","www.example.net"]', '[testGoogleAnalyticsTrackingCode]: Cross domain tracking with www.example.com and www.example.net is active.');
+    variable_set('googleanalytics_domain_mode', 0);
+
+    // Test whether debugging script has been enabled.
+    variable_set('googleanalytics_debug', 1);
+    $this->drupalGet('');
+    $this->assertRaw('https://www.google-analytics.com/analytics_debug.js', '[testGoogleAnalyticsTrackingCode]: Google debugging script has been enabled.');
+
+    // Check if text and link is shown on 'Status Reports' page.
+    // Requires 'administer site configuration' permission.
+    $this->drupalGet('admin/reports/status');
+    $this->assertRaw(t('Google Analytics module has debugging enabled. Please disable debugging setting in production sites from the <a href="@url">Google Analytics settings page</a>.', array('@url' => url('admin/config/system/googleanalytics'))), '[testGoogleAnalyticsConfiguration]: Debugging enabled is shown on Status Reports page.');
 
-    // Test whether the BEFORE and AFTER code is added to the tracker.
-    variable_set('googleanalytics_codesnippet_before', '_setDetectFlash(false);');
-    variable_set('googleanalytics_codesnippet_after', '_gaq.push(["t2._setAccount", "UA-123456-3"]);_gaq.push(["t2._trackPageview"]);');
+    // Test whether debugging script has been disabled.
+    variable_set('googleanalytics_debug', 0);
     $this->drupalGet('');
-    $this->assertRaw('_setDetectFlash(false);', '[testGoogleAnalyticsTrackingCode]: Before codesnippet has been found with "Flash" detection disabled.');
-    $this->assertRaw('t2._setAccount', '[testGoogleAnalyticsTrackingCode]: After codesnippet with "t2" tracker has been found.');
+    $this->assertRaw('https://www.google-analytics.com/analytics.js', '[testGoogleAnalyticsTrackingCode]: Google debugging script has been disabled.');
+
+    // Test whether the CREATE and BEFORE and AFTER code is added to the tracker.
+    $codesnippet_create = array(
+      'cookieDomain' => 'foo.example.com',
+      'cookieName' => 'myNewName',
+      'cookieExpires' => 20000,
+      'allowAnchor' => TRUE,
+      'sampleRate' => 4.3,
+    );
+    variable_set('googleanalytics_codesnippet_create', $codesnippet_create);
+    variable_set('googleanalytics_codesnippet_before', 'ga("set", "forceSSL", true);');
+    variable_set('googleanalytics_codesnippet_after', 'ga("create", "UA-123456-3", {"name": "newTracker"});ga("newTracker.send", "pageview");');
+    $this->drupalGet('');
+    $this->assertRaw('ga("create", "' . $ua_code . '", {"cookieDomain":"foo.example.com","cookieName":"myNewName","cookieExpires":20000,"allowAnchor":true,"sampleRate":4.3});', '[testGoogleAnalyticsTrackingCode]: Create only fields have been found.');
+    $this->assertRaw('ga("set", "forceSSL", true);', '[testGoogleAnalyticsTrackingCode]: Before codesnippet will force http pages to also send all beacons using https.');
+    $this->assertRaw('ga("create", "UA-123456-3", {"name": "newTracker"});', '[testGoogleAnalyticsTrackingCode]: After codesnippet with "newTracker" tracker has been found.');
   }
 }
 
-class GoogleAnalyticsCustomVariablesTest extends DrupalWebTestCase {
+class GoogleAnalyticsCustomDimensionsAndMetricsTest extends DrupalWebTestCase {
 
   public static function getInfo() {
     return array(
-      'name' => t('Google Analytics Custom Variables tests'),
-      'description' => t('Test custom variables functionality of Google Analytics module.'),
+      'name' => 'Google Analytics custom dimensions and metrics tests',
+      'description' => 'Test custom dimensions and metrics functionality of Google Analytics module.',
       'group' => 'Google Analytics',
       'dependencies' => array('token'),
     );
@@ -206,110 +316,217 @@ class GoogleAnalyticsCustomVariablesTest extends DrupalWebTestCase {
 
     // User to set up google_analytics.
     $this->admin_user = $this->drupalCreateUser($permissions);
+    $this->drupalLogin($this->admin_user);
   }
 
-  function testGoogleAnalyticsCustomVariables() {
+  function testGoogleAnalyticsCustomDimensions() {
     $ua_code = 'UA-123456-3';
     variable_set('googleanalytics_account', $ua_code);
 
     // Basic test if the feature works.
-    $custom_vars = array(
-      'slots' => array(
-        1 => array(
-          'slot' => 1,
-          'name' => 'Foo 1',
-          'value' => 'Bar 1',
-          'scope' => 3,
-        ),
-        2 => array(
-          'slot' => 2,
-          'name' => 'Foo 2',
-          'value' => 'Bar 2',
-          'scope' => 2,
-        ),
-        3 => array(
-          'slot' => 3,
-          'name' => 'Foo 3',
-          'value' => 'Bar 3',
-          'scope' => 3,
-        ),
-        4 => array(
-          'slot' => 4,
-          'name' => 'Foo 4',
-          'value' => 'Bar 4',
-          'scope' => 2,
-        ),
-        5 => array(
-          'slot' => 5,
-          'name' => 'Foo 5',
-          'value' => 'Bar 5',
-          'scope' => 1,
-        ),
-      )
+    $googleanalytics_custom_dimension = array(
+      1 => array(
+        'index' => 1,
+        'value' => 'Bar 1',
+      ),
+      2 => array(
+        'index' => 2,
+        'value' => 'Bar 2',
+      ),
+      3 => array(
+        'index' => 3,
+        'value' => 'Bar 3',
+      ),
+      4 => array(
+        'index' => 4,
+        'value' => 'Bar 4',
+      ),
+      5 => array(
+        'index' => 5,
+        'value' => 'Bar 5',
+      ),
     );
-    variable_set('googleanalytics_custom_var', $custom_vars);
+    variable_set('googleanalytics_custom_dimension', $googleanalytics_custom_dimension);
     $this->drupalGet('');
 
-    foreach ($custom_vars['slots'] as $slot) {
-      $this->assertRaw("_gaq.push(['_setCustomVar', " . $slot['slot'] . ", \"" . $slot['name'] . "\", \"" . $slot['value'] . "\", " . $slot['scope'] . "]);", '[testGoogleAnalyticsCustomVariables]: _setCustomVar ' . $slot['slot'] . ' is shown.');
+    foreach ($googleanalytics_custom_dimension as $dimension) {
+      $this->assertRaw('ga("set", ' . drupal_json_encode('dimension' . $dimension['index']) . ', ' . drupal_json_encode($dimension['value']) . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Dimension #' . $dimension['index'] . ' is shown.');
     }
 
-    // Test whether tokens are replaced in custom variable names.
+    // Test whether tokens are replaced in custom dimension values.
     $site_slogan = $this->randomName(16);
     variable_set('site_slogan', $site_slogan);
 
-    $custom_vars = array(
-      'slots' => array(
-        1 => array(
-          'slot' => 1,
-          'name' => 'Name: [site:slogan]',
-          'value' => 'Value: [site:slogan]',
-          'scope' => 3,
-        ),
-        2 => array(
-          'slot' => 2,
-          'name' => '',
-          'value' => $this->randomName(16),
-          'scope' => 1,
-        ),
-        3 => array(
-          'slot' => 3,
-          'name' => $this->randomName(16),
-          'value' => '',
-          'scope' => 2,
-        ),
-        4 => array(
-          'slot' => 4,
-          'name' => '',
-          'value' => '',
-          'scope' => 3,
-        ),
-        5 => array(
-          'slot' => 5,
-          'name' => '',
-          'value' => '',
-          'scope' => 3,
-        ),
-      )
+    $googleanalytics_custom_dimension = array(
+      1 => array(
+        'index' => 1,
+        'value' => 'Value: [site:slogan]',
+      ),
+      2 => array(
+        'index' => 2,
+        'value' => $this->randomName(16),
+      ),
+      3 => array(
+        'index' => 3,
+        'value' => '',
+      ),
+      // #2300701: Custom dimensions and custom metrics not outputed on zero value.
+      4 => array(
+        'index' => 4,
+        'value' => '0',
+      ),
     );
-    variable_set('googleanalytics_custom_var', $custom_vars);
-    $this->verbose('<pre>' . print_r($custom_vars, TRUE) . '</pre>');
+    variable_set('googleanalytics_custom_dimension', $googleanalytics_custom_dimension);
+    $this->verbose('<pre>' . print_r($googleanalytics_custom_dimension, TRUE) . '</pre>');
 
     $this->drupalGet('');
-    $this->assertRaw("_gaq.push(['_setCustomVar', 1, \"Name: $site_slogan\", \"Value: $site_slogan\", 3]", '[testGoogleAnalyticsCustomVariables]: Tokens have been replaced in custom variable.');
-    $this->assertNoRaw("_gaq.push(['_setCustomVar', 2,", '[testGoogleAnalyticsCustomVariables]: Value with empty name is not shown.');
-    $this->assertNoRaw("_gaq.push(['_setCustomVar', 3,", '[testGoogleAnalyticsCustomVariables]: Name with empty value is not shown.');
-    $this->assertNoRaw("_gaq.push(['_setCustomVar', 4,", '[testGoogleAnalyticsCustomVariables]: Empty name and value is not shown.');
-    $this->assertNoRaw("_gaq.push(['_setCustomVar', 5,", '[testGoogleAnalyticsCustomVariables]: Empty name and value is not shown.');
+    $this->assertRaw('ga("set", ' . drupal_json_encode('dimension1') . ', ' . drupal_json_encode("Value: $site_slogan") . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Tokens have been replaced in dimension value.');
+    $this->assertRaw('ga("set", ' . drupal_json_encode('dimension2') . ', ' . drupal_json_encode($googleanalytics_custom_dimension['2']['value']) . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Random value is shown.');
+    $this->assertNoRaw('ga("set", ' . drupal_json_encode('dimension3') . ', ' . drupal_json_encode('') . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Empty value is not shown.');
+    $this->assertRaw('ga("set", ' . drupal_json_encode('dimension4') . ', ' . drupal_json_encode('0') . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Value 0 is shown.');
+  }
+
+  function testGoogleAnalyticsCustomMetrics() {
+    $ua_code = 'UA-123456-3';
+    variable_set('googleanalytics_account', $ua_code);
+
+    // Basic test if the feature works.
+    $googleanalytics_custom_metric = array(
+      1 => array(
+        'index' => 1,
+        'value' => '6',
+      ),
+      2 => array(
+        'index' => 2,
+        'value' => '8000',
+      ),
+      3 => array(
+        'index' => 3,
+        'value' => '7.8654',
+      ),
+      4 => array(
+        'index' => 4,
+        'value' => '1123.4',
+      ),
+      5 => array(
+        'index' => 5,
+        'value' => '5,67',
+      ),
+    );
+
+    variable_set('googleanalytics_custom_metric', $googleanalytics_custom_metric);
+    $this->drupalGet('');
+
+    foreach ($googleanalytics_custom_metric as $metric) {
+      $this->assertRaw('ga("set", ' . drupal_json_encode('metric' . $metric['index']) . ', ' . drupal_json_encode((float) $metric['value']) . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Metric #' . $metric['index'] . ' is shown.');
+    }
+
+    // Test whether tokens are replaced in custom metric values.
+    $googleanalytics_custom_metric = array(
+      1 => array(
+        'index' => 1,
+        'value' => '[current-user:roles:count]',
+      ),
+      2 => array(
+        'index' => 2,
+        'value' => mt_rand(),
+      ),
+      3 => array(
+        'index' => 3,
+        'value' => '',
+      ),
+      // #2300701: Custom dimensions and custom metrics not outputed on zero value.
+      4 => array(
+        'index' => 4,
+        'value' => '0',
+      ),
+    );
+    variable_set('googleanalytics_custom_metric', $googleanalytics_custom_metric);
+    $this->verbose('<pre>' . print_r($googleanalytics_custom_metric, TRUE) . '</pre>');
+
+    $this->drupalGet('');
+    $this->assertRaw('ga("set", ' . drupal_json_encode('metric1') . ', ', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Tokens have been replaced in metric value.');
+    $this->assertRaw('ga("set", ' . drupal_json_encode('metric2') . ', ' . drupal_json_encode($googleanalytics_custom_metric['2']['value']) . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Random value is shown.');
+    $this->assertNoRaw('ga("set", ' . drupal_json_encode('metric3') . ', ' . drupal_json_encode('') . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Empty value is not shown.');
+    $this->assertRaw('ga("set", ' . drupal_json_encode('metric4') . ', ' . drupal_json_encode(0) . ');', '[testGoogleAnalyticsCustomDimensionsAndMetrics]: Value 0 is shown.');
+  }
+
+  /**
+   * Tests if Custom Dimensions token form validation works.
+   */
+  public function testGoogleAnalyticsCustomDimensionsTokenFormValidation() {
+    $ua_code = 'UA-123456-1';
+
+    // Check form validation.
+    $edit['googleanalytics_account'] = $ua_code;
+    $edit['googleanalytics_custom_dimension[indexes][1][value]'] = '[current-user:name]';
+    $edit['googleanalytics_custom_dimension[indexes][2][value]'] = '[current-user:edit-url]';
+    $edit['googleanalytics_custom_dimension[indexes][3][value]'] = '[user:name]';
+    $edit['googleanalytics_custom_dimension[indexes][4][value]'] = '[term:name]';
+    $edit['googleanalytics_custom_dimension[indexes][5][value]'] = '[term:tid]';
+
+    $this->drupalPost('admin/config/system/googleanalytics', $edit, t('Save configuration'));
+
+    $this->assertRaw(t('The %element-title is using the following forbidden tokens with personal identifying information: @invalid-tokens.', array('%element-title' => t('Custom dimension value #@index', array('@index' => 1)), '@invalid-tokens' => implode(', ', array('[current-user:name]')))));
+    $this->assertRaw(t('The %element-title is using the following forbidden tokens with personal identifying information: @invalid-tokens.', array('%element-title' => t('Custom dimension value #@index', array('@index' => 2)), '@invalid-tokens' => implode(', ', array('[current-user:edit-url]')))));
+    $this->assertRaw(t('The %element-title is using the following forbidden tokens with personal identifying information: @invalid-tokens.', array('%element-title' => t('Custom dimension value #@index', array('@index' => 3)), '@invalid-tokens' => implode(', ', array('[user:name]')))));
+    // BUG #2037595
+    //$this->assertNoRaw(t('The %element-title is using the following forbidden tokens with personal identifying information: @invalid-tokens.', array('%element-title' => t('Custom dimension value #@index', array('@index' => 4)), '@invalid-tokens' => implode(', ', array('[term:name]')))));
+    //$this->assertNoRaw(t('The %element-title is using the following forbidden tokens with personal identifying information: @invalid-tokens.', array('%element-title' => t('Custom dimension value #@index', array('@index' => 5)), '@invalid-tokens' => implode(', ', array('[term:tid]')))));
+  }
+}
+
+/**
+ * Test custom url functionality of Google Analytics module.
+ */
+class GoogleAnalyticsCustomUrls extends DrupalWebTestCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Google Analytics custom url tests',
+      'description' => 'Test custom url functionality of Google Analytics module.',
+      'group' => 'Google Analytics',
+    );
+  }
+
+  function setUp() {
+    parent::setUp('googleanalytics');
+
+    $permissions = array(
+      'access administration pages',
+      'administer google analytics',
+    );
+
+    // User to set up google_analytics.
+    $this->admin_user = $this->drupalCreateUser($permissions);
+  }
+
+  /**
+   * Tests if user password page urls are overridden.
+   */
+  public function testGoogleAnalyticsUserPasswordPage() {
+    $base_path = base_path();
+    $ua_code = 'UA-123456-4';
+    variable_set('googleanalytics_account', $ua_code);
+
+    $this->drupalGet('user/password', array('query' => array('name' => 'foo')));
+    $this->assertRaw('ga("set", "page", "' . $base_path . 'user/password"');
+
+    $this->drupalGet('user/password', array('query' => array('name' => 'foo@example.com')));
+    $this->assertRaw('ga("set", "page", "' . $base_path . 'user/password"');
+
+    $this->drupalGet('user/password');
+    $this->assertNoRaw('ga("set", "page",', '[testGoogleAnalyticsCustomUrls]: Custom url not set.');
   }
+
 }
 
 class GoogleAnalyticsStatusMessagesTest extends DrupalWebTestCase {
 
   public static function getInfo() {
     return array(
-      'name' => t('Google Analytics status messages tests'),
-      'description' => t('Test status messages functionality of Google Analytics module.'),
+      'name' => 'Google Analytics status messages tests',
+      'description' => 'Test status messages functionality of Google Analytics module.',
       'group' => 'Google Analytics',
     );
   }
@@ -333,20 +550,20 @@ class GoogleAnalyticsStatusMessagesTest extends DrupalWebTestCase {
     // Enable logging of errors only.
     variable_set('googleanalytics_trackmessages', array('error' => 'error'));
 
-    $this->drupalPost('user/login', array(), 'Log in');
-    $this->assertRaw('_gaq.push(["_trackEvent", "Messages", "Error message", "Username field is required."]);', '[testGoogleAnalyticsStatusMessages]: _trackEvent "Username field is required." is shown.');
-    $this->assertRaw('_gaq.push(["_trackEvent", "Messages", "Error message", "Password field is required."]);', '[testGoogleAnalyticsStatusMessages]: _trackEvent "Password field is required." is shown.');
+    $this->drupalPost('user/login', array(), t('Log in'));
+    $this->assertRaw('ga("send", "event", "Messages", "Error message", "Username field is required.");', '[testGoogleAnalyticsStatusMessages]: Event message "Username field is required." is shown.');
+    $this->assertRaw('ga("send", "event", "Messages", "Error message", "Password field is required.");', '[testGoogleAnalyticsStatusMessages]: Event message "Password field is required." is shown.');
 
     // @todo: investigate why drupal_set_message() fails.
     //drupal_set_message('Example status message.', 'status');
     //drupal_set_message('Example warning message.', 'warning');
     //drupal_set_message('Example error message.', 'error');
-    //drupal_set_message('Example error <em>message</em> with html tags and <a href="http://example.com/">link</a>.', 'error');
+    //drupal_set_message('Example error <em>message</em> with html tags and <a href="https://example.com/">link</a>.', 'error');
     //$this->drupalGet('');
-    //$this->assertNoRaw('_gaq.push(["_trackEvent", "Messages", "Status message", "Example status message."]);', '[testGoogleAnalyticsStatusMessages]: Example status message is not enabled for tracking.');
-    //$this->assertNoRaw('_gaq.push(["_trackEvent", "Messages", "Warning message", "Example warning message."]);', '[testGoogleAnalyticsStatusMessages]: Example warning message is not enabled for tracking.');
-    //$this->assertRaw('_gaq.push(["_trackEvent", "Messages", "Error message", "Example error message."]);', '[testGoogleAnalyticsStatusMessages]: Example error message is shown.');
-    //$this->assertRaw('_gaq.push(["_trackEvent", "Messages", "Error message", "Example error message with html tags and link."]);', '[testGoogleAnalyticsStatusMessages]: HTML has been stripped successful from Example error message with html tags and link.');
+    //$this->assertNoRaw('ga("send", "event", "Messages", "Status message", "Example status message.");', '[testGoogleAnalyticsStatusMessages]: Example status message is not enabled for tracking.');
+    //$this->assertNoRaw('ga("send", "event", "Messages", "Warning message", "Example warning message.");', '[testGoogleAnalyticsStatusMessages]: Example warning message is not enabled for tracking.');
+    //$this->assertRaw('ga("send", "event", "Messages", "Error message", "Example error message.");', '[testGoogleAnalyticsStatusMessages]: Example error message is shown.');
+    //$this->assertRaw('ga("send", "event", "Messages", "Error message", "Example error message with html tags and link.");', '[testGoogleAnalyticsStatusMessages]: HTML has been stripped successful from Example error message with html tags and link.');
   }
 }
 
@@ -354,8 +571,8 @@ class GoogleAnalyticsRolesTest extends DrupalWebTestCase {
 
   public static function getInfo() {
     return array(
-      'name' => t('Google Analytics role tests'),
-      'description' => t('Test roles functionality of Google Analytics module.'),
+      'name' => 'Google Analytics role tests',
+      'description' => 'Test roles functionality of Google Analytics module.',
       'group' => 'Google Analytics',
     );
   }
@@ -438,5 +655,165 @@ class GoogleAnalyticsRolesTest extends DrupalWebTestCase {
     $this->drupalGet('');
     $this->assertRaw($ua_code, '[testGoogleAnalyticsRoleVisibility]: Tracking code is displayed on frontpage for included anonymous users.');
   }
+}
+
+class GoogleAnalyticsSearchTest extends DrupalWebTestCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Google Analytics search tests',
+      'description' => 'Test search functionality of Google Analytics module.',
+      'group' => 'Google Analytics',
+    );
+  }
+
+  function setUp() {
+    parent::setUp('googleanalytics', 'search', 'node');
+
+    $permissions = array(
+      'access administration pages',
+      'administer google analytics',
+      'search content',
+      'create page content',
+      'edit own page content',
+    );
+
+    // User to set up google_analytics.
+    $this->admin_user = $this->drupalCreateUser($permissions);
+    $this->drupalLogin($this->admin_user);
+  }
+
+  function testGoogleAnalyticsSearchTracking() {
+    $ua_code = 'UA-123456-1';
+    variable_set('googleanalytics_account', $ua_code);
+
+    // Check tracking code visibility.
+    $this->drupalGet('');
+    $this->assertRaw($ua_code, '[testGoogleAnalyticsSearch]: Tracking code is displayed for authenticated users.');
+
+    $this->drupalGet('search/node');
+    $this->assertNoRaw('ga("set", "page",', '[testGoogleAnalyticsSearch]: Custom url not set.');
+
+    // Enable site search support.
+    variable_set('googleanalytics_site_search', 1);
+
+    // Search for random string.
+    $search = array();
+    $search['keys'] = $this->randomName(8);
+
+    // Create a node to search for.
+    $langcode = LANGUAGE_NONE;
+    $edit = array();
+    $edit['title'] = 'This is a test title';
+    $edit["body[$langcode][0][value]"] = 'This test content contains ' . $search['keys'] . ' string.';
+
+    // Fire a search, it's expected to get 0 results.
+    $this->drupalPost('search/node', $search, t('Search'));
+    $this->assertRaw('ga("set", "page", (window.googleanalytics_search_results) ?', '[testGoogleAnalyticsSearch]: Search results tracker is displayed.');
+    $this->assertRaw('window.googleanalytics_search_results = 0;', '[testGoogleAnalyticsSearch]: Search yielded no results.');
+
+    // Save the node.
+    $this->drupalPost('node/add/page', $edit, t('Save'));
+    $this->assertText(t('@type @title has been created.', array('@type' => 'Basic page', '@title' => $edit['title'])), 'Node was created.');
+
+    // Index the node or it cannot found.
+    $this->cronRun();
+
+    $this->drupalPost('search/node', $search, t('Search'));
+    $this->assertRaw('ga("set", "page", (window.googleanalytics_search_results) ?', '[testGoogleAnalyticsSearch]: Search results tracker is displayed.');
+    $this->assertRaw('window.googleanalytics_search_results = 1;', '[testGoogleAnalyticsSearch]: One search result found.');
+
+    $this->drupalPost('node/add/page', $edit, t('Save'));
+    $this->assertText(t('@type @title has been created.', array('@type' => 'Basic page', '@title' => $edit['title'])), 'Node was created.');
+
+    // Index the node or it cannot found.
+    $this->cronRun();
+
+    $this->drupalPost('search/node', $search, t('Search'));
+    $this->assertRaw('ga("set", "page", (window.googleanalytics_search_results) ?', '[testGoogleAnalyticsSearch]: Search results tracker is displayed.');
+    $this->assertRaw('window.googleanalytics_search_results = 2;', '[testGoogleAnalyticsSearch]: Two search results found.');
+  }
+
+}
+
+class GoogleAnalyticsPhpFilterTest extends DrupalWebTestCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Google Analytics php filter tests',
+      'description' => 'Test php filter functionality of Google Analytics module.',
+      'group' => 'Google Analytics',
+    );
+  }
+
+  function setUp() {
+    parent::setUp('googleanalytics', 'php');
+
+    // Administrator with all permissions.
+    $permissions_admin_user = array(
+      'access administration pages',
+      'administer google analytics',
+      'use PHP for tracking visibility',
+    );
+    $this->admin_user = $this->drupalCreateUser($permissions_admin_user);
+
+    // Administrator who cannot configure tracking visibility with PHP.
+    $permissions_delegated_admin_user = array(
+      'access administration pages',
+      'administer google analytics',
+    );
+    $this->delegated_admin_user = $this->drupalCreateUser($permissions_delegated_admin_user);
+  }
+
+  function testGoogleAnalyticsPhpFilter() {
+    $ua_code = 'UA-123456-1';
+    $this->drupalLogin($this->admin_user);
+
+    $edit = array();
+    $edit['googleanalytics_account'] = $ua_code;
+    $edit['googleanalytics_visibility_pages'] = 2;
+    $edit['googleanalytics_pages'] = '<?php return 0; ?>';
+    $this->drupalPost('admin/config/system/googleanalytics', $edit, t('Save configuration'));
+
+    // Compare saved setting with posted setting.
+    $googleanalytics_pages = variable_get('googleanalytics_pages', $this->randomName(8));
+    $this->assertEqual('<?php return 0; ?>', $googleanalytics_pages, '[testGoogleAnalyticsPhpFilter]: PHP code snippet is intact.');
+
+    // Check tracking code visibility.
+    variable_set('googleanalytics_pages', '<?php return TRUE; ?>');
+    $this->drupalGet('');
+    $this->assertRaw('https://www.google-analytics.com/analytics.js', '[testGoogleAnalyticsPhpFilter]: Tracking is displayed on frontpage page.');
+    $this->drupalGet('admin');
+    $this->assertRaw('https://www.google-analytics.com/analytics.js', '[testGoogleAnalyticsPhpFilter]: Tracking is displayed on admin page.');
+
+    variable_set('googleanalytics_pages', '<?php return FALSE; ?>');
+    $this->drupalGet('');
+    $this->assertNoRaw('https://www.google-analytics.com/analytics.js', '[testGoogleAnalyticsPhpFilter]: Tracking is not displayed on frontpage page.');
+
+    // Test administration form.
+    variable_set('googleanalytics_pages', '<?php return TRUE; ?>');
+    $this->drupalGet('admin/config/system/googleanalytics');
+    $this->assertRaw(t('Pages on which this PHP code returns <code>TRUE</code> (experts only)'), '[testGoogleAnalyticsPhpFilter]: Permission to administer PHP for tracking visibility.');
+    $this->assertRaw(check_plain('<?php return TRUE; ?>'), '[testGoogleAnalyticsPhpFilter]: PHP code snippted is displayed.');
+
+    // Login the delegated user and check if fields are visible.
+    $this->drupalLogin($this->delegated_admin_user);
+    $this->drupalGet('admin/config/system/googleanalytics');
+    $this->assertNoRaw(t('Pages on which this PHP code returns <code>TRUE</code> (experts only)'), '[testGoogleAnalyticsPhpFilter]: No permission to administer PHP for tracking visibility.');
+    $this->assertNoRaw(check_plain('<?php return TRUE; ?>'), '[testGoogleAnalyticsPhpFilter]: No permission to view PHP code snippted.');
+
+    // Set a different value and verify that this is still the same after the post.
+    variable_set('googleanalytics_pages', '<?php return 0; ?>');
+
+    $edit = array();
+    $edit['googleanalytics_account'] = $ua_code;
+    $this->drupalPost('admin/config/system/googleanalytics', $edit, t('Save configuration'));
+
+    // Compare saved setting with posted setting.
+    $googleanalytics_visibility_pages = variable_get('googleanalytics_visibility_pages', 0);
+    $googleanalytics_pages = variable_get('googleanalytics_pages', $this->randomName(8));
+    $this->assertEqual(2, $googleanalytics_visibility_pages, '[testGoogleAnalyticsPhpFilter]: Pages on which this PHP code returns TRUE is selected.');
+    $this->assertEqual('<?php return 0; ?>', $googleanalytics_pages, '[testGoogleAnalyticsPhpFilter]: PHP code snippet is intact.');
+  }
 
 }

+ 121 - 0
sites/all/modules/contrib/admin/google_analytics/googleanalytics.test.js

@@ -0,0 +1,121 @@
+(function ($) {
+
+/**
+ *  This file is for developers only.
+ *
+ *  This tests are made for the javascript functions used in GA module.
+ *  These tests verify if the return values are properly working.
+ *
+ *  Hopefully this can be added somewhere else once Drupal core has JavaScript
+ *  unit testing integrated.
+ */
+
+"use strict";
+
+Drupal.googleanalytics.test = {};
+
+Drupal.googleanalytics.test.assertSame = function (value1, value2, message) {
+  if (value1 === value2) {
+    console.info(message);
+  }
+  else {
+    console.error(message);
+  }
+};
+
+Drupal.googleanalytics.test.assertNotSame = function (value1, value2, message) {
+  if (value1 !== value2) {
+    console.info(message);
+  }
+  else {
+    console.error(message);
+  }
+};
+
+Drupal.googleanalytics.test.assertTrue = function (value1, message) {
+  if (value1 === true) {
+    console.info(message);
+  }
+  else {
+    console.error(message);
+  }
+};
+
+Drupal.googleanalytics.test.assertFalse = function (value1, message) {
+  if (value1 === false) {
+    console.info(message);
+  }
+  else {
+    console.error(message);
+  }
+};
+
+// Run after the documented is ready or Drupal.settings is undefined.
+$(document).ready(function() {
+
+  /**
+   *  Run javascript tests against the GA module.
+   */
+
+  // JavaScript debugging
+  var base_url = window.location.protocol + '//' + window.location.host;
+  var base_path = window.location.pathname;
+  console.dir(Drupal);
+
+  console.group("Test 'isDownload':");
+  Drupal.googleanalytics.test.assertFalse(Drupal.googleanalytics.isDownload(base_url + Drupal.settings.basePath + 'node/8'), "Verify that '/node/8' url is not detected as file download.");
+  Drupal.googleanalytics.test.assertTrue(Drupal.googleanalytics.isDownload(base_url + Drupal.settings.basePath + 'files/foo1.zip'), "Verify that '/files/foo1.zip' url is detected as a file download.");
+  Drupal.googleanalytics.test.assertTrue(Drupal.googleanalytics.isDownload(base_url + Drupal.settings.basePath + 'files/foo1.zip#foo'), "Verify that '/files/foo1.zip#foo' url is detected as a file download.");
+  Drupal.googleanalytics.test.assertTrue(Drupal.googleanalytics.isDownload(base_url + Drupal.settings.basePath + 'files/foo1.zip?foo=bar'), "Verify that '/files/foo1.zip?foo=bar' url is detected as a file download.");
+  Drupal.googleanalytics.test.assertTrue(Drupal.googleanalytics.isDownload(base_url + Drupal.settings.basePath + 'files/foo1.zip?foo=bar#foo'), "Verify that '/files/foo1.zip?foo=bar#foo' url is detected as a file download.");
+  Drupal.googleanalytics.test.assertFalse(Drupal.googleanalytics.isDownload(base_url + Drupal.settings.basePath + 'files/foo2.ddd'), "Verify that '/files/foo2.ddd' url is not detected as file download.");
+  console.groupEnd();
+
+  console.group("Test 'isInternal':");
+  Drupal.googleanalytics.test.assertTrue(Drupal.googleanalytics.isInternal(base_url + Drupal.settings.basePath + 'node/1'), "Link '" + base_url + Drupal.settings.basePath + "node/2' has been detected as internal link.");
+  Drupal.googleanalytics.test.assertTrue(Drupal.googleanalytics.isInternal(base_url + Drupal.settings.basePath + 'node/1#foo'), "Link '" + base_url + Drupal.settings.basePath + "node/1#foo' has been detected as internal link.");
+  Drupal.googleanalytics.test.assertTrue(Drupal.googleanalytics.isInternal(base_url + Drupal.settings.basePath + 'node/1?foo=bar'), "Link '" + base_url + Drupal.settings.basePath + "node/1?foo=bar' has been detected as internal link.");
+  Drupal.googleanalytics.test.assertTrue(Drupal.googleanalytics.isInternal(base_url + Drupal.settings.basePath + 'node/1?foo=bar#foo'), "Link '" + base_url + Drupal.settings.basePath + "node/1?foo=bar#foo' has been detected as internal link.");
+  Drupal.googleanalytics.test.assertTrue(Drupal.googleanalytics.isInternal(base_url + Drupal.settings.basePath + 'go/foo'), "Link '" + base_url + Drupal.settings.basePath + "go/foo' has been detected as internal link.");
+  Drupal.googleanalytics.test.assertFalse(Drupal.googleanalytics.isInternal('https://example.com/node/3'), "Link 'https://example.com/node/3' has been detected as external link.");
+  console.groupEnd();
+
+  console.group("Test 'isInternalSpecial':");
+  Drupal.googleanalytics.test.assertTrue(Drupal.googleanalytics.isInternalSpecial(base_url + Drupal.settings.basePath + 'go/foo'), "Link '" + base_url + Drupal.settings.basePath + "go/foo' has been detected as special internal link.");
+  Drupal.googleanalytics.test.assertFalse(Drupal.googleanalytics.isInternalSpecial(base_url + Drupal.settings.basePath + 'node/1'), "Link '" + base_url + Drupal.settings.basePath + "node/1' has been detected as special internal link.");
+  console.groupEnd();
+
+  console.group("Test 'getPageUrl':");
+  Drupal.google_analytics.test.assertSame(base_path, Drupal.google_analytics.getPageUrl(window.location.href), "Absolute internal URL '" + base_path + "' has been extracted from full qualified url '" + window.location.href + "'.");
+  Drupal.google_analytics.test.assertSame(base_path, Drupal.google_analytics.getPageUrl(base_path), "Absolute internal URL '" + base_path + "' has been extracted from absolute url '" + base_path + "'.");
+  //Drupal.googleanalytics.test.assertSame(base_path, Drupal.googleanalytics.getPageUrl(base_url + Drupal.settings.basePath + 'node/1'), "Absolute internal URL '" +  Drupal.settings.basePath + "node/1' has been extracted from full qualified url '" + base_url + base_path + "'.");
+  //Drupal.googleanalytics.test.assertSame(base_path, Drupal.googleanalytics.getPageUrl(Drupal.settings.basePath + 'node/1'), "Absolute internal URL '" +  Drupal.settings.basePath + "node/1' has been extracted from absolute url '" +  base_path + "'.");
+  Drupal.googleanalytics.test.assertSame('https://example.com/node/2', Drupal.googleanalytics.getPageUrl('https://example.com/node/2'), "Full qualified external url 'https://example.com/node/2' has been extracted.");
+  Drupal.googleanalytics.test.assertSame('//example.com/node/2', Drupal.googleanalytics.getPageUrl('//example.com/node/2'), "Full qualified external url '//example.com/node/2' has been extracted.");
+  console.groupEnd();
+
+  console.group("Test 'getDownloadExtension':");
+  Drupal.googleanalytics.test.assertSame('zip', Drupal.googleanalytics.getDownloadExtension(base_url + Drupal.settings.basePath + '/files/foo1.zip'), "Download extension 'zip' has been found in '" + base_url + Drupal.settings.basePath + "files/foo1.zip'.");
+  Drupal.googleanalytics.test.assertSame('zip', Drupal.googleanalytics.getDownloadExtension(base_url + Drupal.settings.basePath + '/files/foo1.zip#foo'), "Download extension 'zip' has been found in '" + base_url + Drupal.settings.basePath + "files/foo1.zip#foo'.");
+  Drupal.googleanalytics.test.assertSame('zip', Drupal.googleanalytics.getDownloadExtension(base_url + Drupal.settings.basePath + '/files/foo1.zip?foo=bar'), "Download extension 'zip' has been found in '" + base_url + Drupal.settings.basePath + "files/foo1.zip?foo=bar'.");
+  Drupal.googleanalytics.test.assertSame('zip', Drupal.googleanalytics.getDownloadExtension(base_url + Drupal.settings.basePath + '/files/foo1.zip?foo=bar#foo'), "Download extension 'zip' has been found in '" + base_url + Drupal.settings.basePath + "files/foo1.zip?foo=bar'.");
+  Drupal.googleanalytics.test.assertSame('', Drupal.googleanalytics.getDownloadExtension(base_url + Drupal.settings.basePath + '/files/foo2.dddd'), "No download extension found in '" + base_url + Drupal.settings.basePath + "files/foo2.dddd'.");
+  console.groupEnd();
+
+  // List of top-level domains: example.com, example.net
+  console.group("Test 'isCrossDomain' (requires cross domain configuration with 'example.com' and 'example.net'):");
+  if (Drupal.settings.googleanalytics.trackCrossDomains) {
+    console.dir(Drupal.settings.googleanalytics.trackCrossDomains);
+    Drupal.googleanalytics.test.assertTrue(Drupal.googleanalytics.isCrossDomain('example.com', Drupal.settings.googleanalytics.trackCrossDomains), "URL 'example.com' has been found in cross domain list.");
+    Drupal.googleanalytics.test.assertTrue(Drupal.googleanalytics.isCrossDomain('example.net', Drupal.settings.googleanalytics.trackCrossDomains), "URL 'example.net' has been found in cross domain list.");
+    Drupal.googleanalytics.test.assertFalse(Drupal.googleanalytics.isCrossDomain('www.example.com', Drupal.settings.googleanalytics.trackCrossDomains), "URL 'www.example.com' not found in cross domain list.");
+    Drupal.googleanalytics.test.assertFalse(Drupal.googleanalytics.isCrossDomain('www.example.net', Drupal.settings.googleanalytics.trackCrossDomains), "URL 'www.example.net' not found in cross domain list.");
+  }
+  else {
+    console.warn('Cross domain tracking is not enabled. Tests skipped.');
+  }
+  console.groupEnd();
+
+});
+
+})(jQuery);

File diff suppressed because it is too large
+ 0 - 0
sites/all/modules/contrib/admin/google_analytics/googleanalytics.variable.inc


+ 187 - 35
sites/all/modules/contrib/admin/module_filter/CHANGELOG.txt

@@ -1,41 +1,193 @@
-Module Filter 7.x-1.7, 2012-07-05
+Module Filter 7.x-2.1, 2017-06-09
 ---------------------------------
+Issue #2437439 by mikhail.krainiuk, greenSkin, jayhawkfan75: Module Filter does
+  not care about anchors in permission links.
+Issue #2866236 by Munavijayalakshmi, dhruveshdtripathi: Typo error in
+  README.TXT file.
+Issue #2452067 by Madis: Option to show description expanded as default
+  not working.
+Issue #2580791 by makbul_khan8: Coding standards and few function without
+  help comments.
+Issue #2153697 by annya: Disabling option "Number of enabled modules" breaks
+  tabs functionality.
+Issue #1710230 by willvincent: On | Off buttons does not change state with
+  jquery_update module active.
+Added option to show description as expanded by default.
+Improved description field so when it is open, interacting with it's contents
+  does not make it collapse.
+Placed collapsed/expanded images inside of module for easier, more reliable
+  access.
+Added option to place version in own column.
+Issue #2113191 by joelpittet: Category tabs not working.
+
+
+Module Filter 7.x-2.0, 2015-02-22
+---------------------------------
+Simplifying the table rows by hiding version and requirements until a
+  particular description is clicked.
+#2235553 by greenSkin: Fixed latest jquery_update breaks module filter.
+#2304687 by mpdonadio: Fixed Remove hardcoded operations.
+#2293029 by topsitemakers: Fixed Take header offset into account when selecting
+  a tab.
+#2290213 by topsitemakers: Minor typo in description - "has no affect" -> "has
+  no effect".
+#2141743, #2141743 by greenSkin: Fixed issues related to the new dynamically
+  positioned tabs and using the dynamically positioned save button.
+by greenSkin: Tabs now should always be visible while scrolling large lists of
+  modules.
+#1854348 by alexweber, greenSkin: Make filter textfield wider on modules page
+  when viewing as tabs.
+by greenSkin: Fixed what was suppose to be a call to variable_set().
+#1370492 by greenSkin: Remember selected tab after modules form submit.
+by greenSkin: Changed the title for the 'module_filter_dynamic_save_position'
+  checkbox to make it clearer what it does.
+#1166414 by greenSkin: Fixed broken submit when tabs are disabled. We no longer
+  reroute the theme of the system modules page unless tabs are enabled.
+by greenSkin: Fixed issue on Available updates page where the update state
+  would not be remembered.
+by greenSkin: Fixed 7200 update to rebuild the theme registry at the same time
+  as rebuilding the menu.
+by greenSkin: Fixed issue relating to row coloring when enabling/disabling
+  modules via switch due to recent fix for jQuery Update module.
+by greenSkin: Added functionality to show recently enabled/disabled modules.
+#1710230 by littlekoala, greenSkin: Fixed On | Off buttons does not change
+  state with jquery_update() module active.
+by greenSkin: Added description to filter textfield on permissions page.
+by greenSkin: Added filter to user permissions page.
+by greenSkin: Fixed styling when JavaScript is disabled.
+by greenSkin: Fixed compatiblility with page_actions.
+#1351184 klonos, greenSkin: Added support for update_advanced "Ignored from
+  settings".
+#1149978 by good_man, greenSkin: Added RTL Styling for tabs and toggle switch.
+by greenSkin: Hide toggle switch when JavaScript is disabled.
+by greenSkin: Added support for ctools dropbutton as well as views styling for
+  ctools dropbutton.
+by greenSkin: Improved hash validation.
+by greenSkin: Added ability for tabs to be disabled. Direct use case is when
+  the "New" tab contains zero new modules.
+by greenSkin: Added title to "New" tab link that helps to describe the criteria
+  of a "new" module.
+#1320796 by greenSkin: Added some validation checks before trying to select a
+  tab in case the tab does not actually exist.
+#1429248 by klonos, greenSkin: Fixed Modules page table header overlaps
+  admin_menu().
+#1494694 by greenSkin: Added Let me decide if I want the cursor to focus on the
+  search box or not.
+#1515256 by catmat, greenSkin: Fixed Tabbed theme may remove functional
+  content.
+by greenSkin: Integrated dynamic positioning of the save button back in, but
+  if the module page_actions is enabled we let it handle the save button. See
+  issue #1424994.
+by greenSkin: Clicking on module name now affects the toggle switch.
+by greenSkin: Do not render the switch for incompatible modules.
 #1033012 by greenSkin: Hide incompatible module rows when the 'Unavailable'
   checkbox is unchecked.
-#1170388 by greenSkin: Fixed conflict with Overlay module. Added class
-  "overlay-exclude" to tab links.
+by greenSkin: Performance tweak to counting the number of enabled modules.
+by greenSkin: Base new modules on the filectime of their .info file.
+#1424034 by greenSkin: Now adds the jquery.cookie.js file when needed.
+by greenSkin: Removed tweaks to the save configuration button in favor
+  http://drupal.org/project/page_actions.
+by greenSkin: Updated hook_uninstall to del all Module Filter variables.
+by greenSkin: No longer including machine name in the tab summary of modules to
+  enable/disable.
+by greenSkin: Made switches honor disabled checkboxes. A disabled switch can
+  not be turned on or off.
+by greenSkin: Added setting to toggle between using the switch or checkbox for
+  enabling/disabling modules.
+by greenSkin: Centered enable switch.
+by greenSkin: Moved couple styles to be applied via JavaScript instead of CSS
+  so they get applied once the initial loading has finished.
+by greenSkin: Implemented a "switch" look instead of checkboxes.
+by greenSkin: Remember last selected state on "Available updates" page.
+by greenSkin: Switched to using filectime() rather than filemtime() for
+  determining new modules.
+#1354134 by klonos, greenSkin: Module list now formats correctly in core
+  "Garland" theme.
+#1354134 by klonos, greenSkin: Module list now formats correctly in core
+  "Garland" theme.
 #1124218 by jyee, greenSkin: Suppress form submission when hitting the enter
   key while the filter input is focused.
-
-
-Module Filter 7.x-1.6, 2011-09-15
----------------------------------
-#1241662 by Niklas Fiekas: Sort modules by display name.
+by greenSkin: Simplified filter rules for updates page. Instead of checkboxes,
+  now using radios.
+by greenSkin: Fixed "All" tab to be selected by default when the page is first
+  loaded.
+#1350124 by greenSkin: Fixed filtering on package tab.
+by greenSkin: Added the "All" tab back. Added a "New" tab that lists modules
+  installed within the last week.
+by greenSkin: Fixed "No Results" not showing within selected tabs.
+#1259876 by greenSkin: Filter criteria remembered after save.
+by greenSkin: Remove deprecated function moduleGetID() from JavaScript code.
+by greenSkin: Filter now uses OR instead of AND when filtering multiply
+  queries.
+#1288590 by greenSkin: Fixed Drupal.settings.moduleFilter.enabledCounts[id] is
+  undefined.
+#1344214 by eMPee584: Fixed Notice: Undefined index: #default_value in
+  theme_module_filter_system_modules_tabs().
+#1170388 by greenSkin: Fixed confict with Overlay module. Added class
+  "overlay-exclude" to tab links. Added setting to toggle the use of a URL
+  fragment when selecting tabs.
+by greenSkin: Added README.txt.
+by greenSkin: Fixed table row striping.
+by greenSkin: Fixed regular expression to not require an operator be at the
+  beginning. Make filter queries filter with AND instead of OR. Each query will
+  further filter the list rather than potentially add to it.
+by greenSkin: Spruced up the regular expression for splitting query strings.
+by greenSkin: Added "description:" operator.
+by greenSkin: Added operator support to filter. Added "requires:" and
+  "requiredBy:" operators.
+by greenSkin: Filter now processes multiple queries separated by spaces. Use
+  quotes for a single query that includes space(s).
+by greenSkin: Fixed not updating the index when a module is enabled/disabled.
+by greenSkin: Fixed visual aid for enabling/disabling modules. Previously had
+  failed to remove +/- from tab summary.
+by Kiphaas7, greenSkin: Tabs can now be configured to hide when they contain
+  no results.
+by Kiphaas7, greenSkin: Added result info to tabs. When a filter is performed,
+  a count per tab is displayed of the number of visible results for that tab.
+by greenSkin: Now more descriptive of what modules are being enabled/disabled.
+by greenSkin: Moved module operation links to below "Requires" and
+  "Required by" section.
+by greenSkin: Added a suggest class to tabs when their module is hovered.
+by greenSkin: Distinguished difference between tab ID and hash.
+by greenSkin: Only alter the menu item 'admin/reports/updates' if it first
+  exists.
+by greenSkin: Added update to force a menu rebuild. This is needed to let
+  Module Filter alter the Update Status menu item in order to provide our
+  filter on its page.
 #1254140 by greenSkin: No longer return anything in hook_update_7100.
-by greenSkin: Fixed bug with visual aids sometimes not updating correctly.
-
-
-Module Filter 7.x-1.5, 2011-08-16
----------------------------------
-by greenSkin: Brought the 7.x branch current with the 6.x branch features.
-
-
-Module Filter 7.x-1.3, 2011-03-07
----------------------------------
-by realityloop: Updated CHANGELOG.txt
-
-
-Module Filter 7.x-1.2, 2011-03-07
----------------------------------
-by realityloop: Changed placement of Submit buttons for other languages
-
-
-Module Filter 7.x-1.1, 2011-03-07
----------------------------------
-by realityloop: first commit via git, broke the release somehow :/
-
-
-Module Filter 7.x-1.0, 2011-01-04
----------------------------------
-by realityloop: Fixed Undefined index error.
-by greenSkin: Removed unused .css and .js files.
+#1257860 by greenSkin: Added filter to update status report.
+by greenSkin: Moved hiding of the inputs wrapper to css rather than a style
+  attribute.
+by greenSkin: Added missing semi-colons in JavaScript.
+by greenSkin: Changed hook comment from using "Implementation of" to
+  "Implements".
+by greenSkin: Fixed showing all modules by default when no hash is present.
+by greenSkin: Turned off autocomplete for filter textfield.
+by greenSkin: Now using "all" for hash when no tab is selected.
+by greenSkin: Implemented visual aids (displays number of modules being
+  enabled/disabled as well as coloring the modules row accordingly).
+by greenSkin: Implemented the enabled count (displays the number of enabled
+  modules of total for a package.
+by greenSkin: Updated tabs setting description.
+by greenSkin: Added missing period.
+by greenSkin: Made dynamic save position default to on. Updated element title
+  on admin page and removed "DEVELOPMENTAL" from description.
+by greenSkin: Improved fixed-top positioning when toolbar is enabled.
+by greenSkin: Added fixed classes for submit button wrapper.
+by greenSkin: Changed module-filter-tabs from a class to an id.
+by greenSkin: Fixed filter on modules page when tabs are disabled.
+by greenSkin: Set min-height to #module-filter-modules.
+by greenSkin: Implemented module_filter element and using attached js and css
+  more.
+by greenSkin: Filter input and checkboxes can now have their default values set
+  based on query params.
+by greenSkin: Tabs now use URL fragments.
+by greenSkin: Fixed regular expression used to determine tab ID.
+by greenSkin: Tabs have been re-written and are functioning.
+by greenSkin: Added Module Filter to "Administration" package.
+by greenSkin: Improved filtering performance.
+by greenSkin: New filter code.
+by greenSkin: Initial tab layout modified. Modules are all in one table but
+  look like they are in packages. All JavaScript has to do on load now is
+  remove the package name and header rows from tbody then sort the rows.
+by greenSkin: Modified the menu item's description.

+ 107 - 0
sites/all/modules/contrib/admin/module_filter/README.txt

@@ -0,0 +1,107 @@
+Description
+-----------
+This module provides a method for filtering modules on the modules page as well
+as for filtering projects on the update status report.
+
+The supplied filter is simpler than using your browsers find feature which
+searches the entire page. The provided filter will filter modules/projects that
+do not meet your input.
+
+Along with the filter textfield there are additional
+checkboxes that help to narrow the search more. The modules page contains four
+checkboxes: Enabled, Disabled, Required, and Unavailable. While the first two
+are self-explanatory, the latter two can take an explanation. The Required
+checkbox affects visibility of modules that are enabled and have other
+module(s) that require it also enabled. The Unavailable checkbox affects
+visibility of modules that are disabled and depend on module(s) that are
+missing.
+
+The update status report filter also contains four checkboxes: Up-to-Date,
+Update available, Security update, and Unknown. These directly affect the
+visibility of each project; whether it is up-to-date, there is an update
+available, a security update is available, or the status is unknown.
+
+Installation
+------------
+To install this module, do the following:
+
+1. Extract the tar ball that you downloaded from Drupal.org.
+
+2. Upload the entire directory and all its contents to your modules directory.
+
+Configuration
+-------------
+To enable and configure this module do the following:
+
+1. Go to Admin -> Modules, and enable Module Filter.
+
+2. Go to Admin -> Configuration -> User interface -> Module filter, and make
+   any necessary configuration changes. 
+
+Tabs
+----
+By default Module Filter alters the modules page into tabs (Can be disabled on
+configuration page). In the tabs view, each package is converted to a vertical
+tab rather than a fieldset which greatly increases the ability to browse them.
+
+There are several benefits to using the tabs view over the standard view for
+the modules page. I've listed the key benefits below as well as additional
+information that pertains to each.
+
+1.  The increased ease of browsing between packages.
+
+2.  Allows all modules to be listed alphabetically outside of their package,
+    making it all the easier to find the module by name rather than package it
+    happens to be in.
+
+3.  The operations for a module are moved within the description column giving
+    the description more "elbow room".
+
+4.  Filtering is restricted to within the active tab or globally when no tab is
+    selected. By default no tab is selected which will list all modules. When a
+    tab is active and you want to get back to the 'all' state click on the
+    active tab to deselect it.
+
+5.  The number of enabled modules per tab is shown on the active tab. (Can be
+    disabled on configuration page)
+
+6.  Nice visual aids become available showing what modules are to be
+    enabled/disabled and the number of matching modules in each tab when
+    filtering. (Can be disabled on configuration page)
+
+7.  The save configuration button becomes more accessible, either staying at
+    the bottom of the window when the tabs exceed past the bottom and at the
+    top when scrolling past the tabs. (Can be disabled on configuration page)
+
+8.  When filtering, tabs that do not contain matches can be hidden. (Can be
+    enabled on configuration page)
+
+9.  Tab states are remembered like individual pages allowing you to move
+    forward and backward within your selections via your browsers
+    forward/backward buttons.
+
+10. When viewing all modules (no active tab) and mousing over modules it's tab
+    becomes highlighted to signify which tab it belongs to.
+
+Filter operators
+----------------
+The modules page's filter has three filter operators available. Filter
+operators allow alternative filtering techniques. A filter operator is applied
+by typing within the filter textfield 'operator:' (where operator is the
+operator type) followed immediately with the string to pass to the operator
+function (e.g. 'requires:block'). The available operators are:
+
+description:
+   Filter based on a module's description.
+
+requiredBy:
+   Filter based on what a module is required by.
+
+requires:
+   Filter based on what a module requires.
+
+Multiple filters (or queries) can be applied by space delimiting. For example,
+the filter string 'description:ctools views' would filter down to modules with
+"ctools" in the description and "views" within the module's name. To pass a
+space within a single query wrap it within double quotes (e.g. 'requires:"chaos
+tools"' or '"bulk export"').

+ 23 - 0
sites/all/modules/contrib/admin/module_filter/css/dynamic_position.css

@@ -0,0 +1,23 @@
+html.js #module-filter-submit {
+  background-color: #F6F6F6;
+  width: 239px;
+  border: 1px solid #ccc;
+  border-top: 0;
+}
+html.js #module-filter-submit .form-actions {
+  text-align: center;
+  margin: 0;
+}
+html.js #module-filter-submit input {
+  margin: 2em 0 1em;
+}
+html.js #module-filter-submit.fixed {
+  position: fixed;
+  border-top: 1px solid #ccc;
+}
+html.js #module-filter-submit.fixed-top {
+  top: 0;
+}
+html.js #module-filter-submit.fixed-bottom {
+  bottom: 0;
+}

+ 23 - 1
sites/all/modules/contrib/admin/module_filter/css/module_filter.css

@@ -1,4 +1,26 @@
-
+.module-filter-inputs-wrapper {
+  display: none;
+}
+.module-filter-clear {
+  display: inline;
+  position: relative;
+}
+.module-filter-clear a {
+  margin-left: 5px;
+  font-size: 11px;
+  position: absolute;
+}
 #module-filter-show-wrapper .form-item {
   display: inline;
 }
+.module-filter-no-results {
+  text-align: center;
+  text-transform: uppercase;
+  color: #888;
+}
+#module-filter-modules {
+  position: relative;
+}
+#module-filter-modules table {
+  position: absolute;
+}

+ 54 - 0
sites/all/modules/contrib/admin/module_filter/css/module_filter_tab-rtl.css

@@ -0,0 +1,54 @@
+#module-filter-tabs {
+  float: right;
+}
+#module-filter-tabs li.selected a,
+#module-filter-tabs li.selected a:hover,
+#module-filter-tabs li.selected a:focus,
+#module-filter-tabs li.selected a:active {
+  background-color: #fff;
+  margin-left: -1px;
+  margin-right: 0;
+}
+.admin-operations {
+  float: left;
+}
+#module-filter-modules {
+  margin-right: 240px;
+}
+html.js .toggle-enable {
+  background-image: -webkit-gradient(linear, 100% 0%, 0% 0%, color-stop(50%, red), color-stop(50%, orange), color-stop(100%, orange));
+  background-image: -moz-linear-gradient(right, red 50%, orange 50%, orange 100%);
+  background-image: linear-gradient(right, red 50%, orange 50%, orange 100%);
+}
+html.js .toggle-enable.enabled {
+  background-image: -webkit-gradient(linear, 100% 0%, 0% 0%, color-stop(50%, orange), color-stop(50%, green), color-stop(100%, green));
+  background-image: -moz-linear-gradient(right, orange 50%, green 50%, green 100%);
+  background-image: linear-gradient(right, orange 50%, green 50%, green 100%);
+}
+html.js .toggle-enable.disabled {
+  background: #ccc;
+  border-color: #ddd;
+  cursor: auto;
+}
+html.js .toggle-enable.disabled div {
+  background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(0%, #FEFEFE), color-stop(100%, #EEEEEE));
+  background-image: -moz-linear-gradient(top, #FEFEFE 0%, #EEEEEE 100%);
+  background-image: linear-gradient(top, #FEFEFE 0%, #EEEEEE 100%);
+}
+html.js .toggle-enable div {
+  -webkit-transition: right 0.2s;
+  -mox-transition: right 0.2s;
+  -o-transition: right 0.2s;
+  transition: right 0.2s;
+}
+html.js .toggle-enable div:before {
+  content: "ON";
+  left: -24px;
+}
+html.js .toggle-enable div:after {
+  content: "OFF";
+  left: -24px;
+}
+html.js .toggle-enable.off div {
+  right: 24px;
+}

+ 218 - 95
sites/all/modules/contrib/admin/module_filter/css/module_filter_tab.css

@@ -1,136 +1,259 @@
+.sticky-header {
+  z-index: 1;
+}
 
-#module-filter-wrapper .form-item {
-  border: 0px none;
+#module-filter-tabs {
+  float: left;
+}
+#module-filter-tabs ul {
+  width: 239px;
+  list-style: none;
+  list-style-image: none;
+  background-color: #ddd;
+  border: 1px solid #ccc;
+  border-top: none;
   margin: 0;
-  padding: 9px;
+  padding: 0;
+  line-height: 1;
 }
-#module-filter-wrapper .form-item:after {
-/*  display: block;*/
-  clear: none;
+#module-filter-tabs li {
+  background: #eee;
+  border-top: 1px solid #ccc;
+  padding: 0;
+  margin: 0;
+  min-width: 0;
 }
-#module-filter-left {
-  float: left;
-  background-color: #F6F6F6;
-  border: 1px solid #D6DBDE;
-  margin-right: -1px;
-  width: 185px;
+#module-filter-tabs li#new-tab {
+  margin-bottom: 10px;
+  border-bottom: 1px solid #ccc;
 }
-#module-filter-left ul {
-  margin: 0px;
-  padding: 0px;
-  list-style: none;
+#module-filter-tabs li.disabled,
+#module-filter-tabs li#new-tab.disabled {
+  pointer-events: none;
+  cursor: default;
+  background: #ccc;
+  border-top-color: #bbb;
+  border-bottom-color: #bbb;
 }
-#module-filter-left ul li {
-  background: #EFEFEF none repeat scroll 0 0;;
-  border-bottom: 1px solid #D6DBDE;
-  margin: 0px;
-  padding: 0px;
-  list-style-image: none;
+#module-filter-tabs li.disabled a,
+#module-filter-tabs li.disabled span {
+  color: #999;
 }
-#module-filter-left ul li.active {
-  margin-right: -1px;
-  width: 186px;
-  background-color: #FFFFFF;
-  position: relative;
+#module-filter-tabs li.suggest {
+  background: #F9F9F9;
 }
-#module-filter-left ul li a {
-  color: #777777;
+#module-filter-tabs li a {
   display: block;
-  padding: 0.5em;
-  line-height: 100%;
-  font-size: 90%;
-  outline: none;
-}
-#module-filter-left ul li.active a {
-  background-color: #FFFFFF;
-  color: #000000;
-  font-weight: bold;
-}
-#module-filter-left ul li a:hover {
-  background-color: #F6F6F6;
   text-decoration: none;
+  padding: 10px;
 }
-#module-filter-left ul li.active a:hover {
-  background-color: #FFFFFF;
+#module-filter-tabs li a span.result-info {
+  float: right;
+  font-size: 10px;
+  color: #999;
+  margin-top: 3px;
 }
-#module-filter-left ul li a span.visual-aid {
+#module-filter-tabs li a span.visual-aid {
   font-size: 8px;
-  float: right;
+/*  float: right;*/
 }
-#module-filter-left ul li a span.enabling {
+#module-filter-tabs li span.visual-aid {
+  font-weight: bold;
+}
+#module-filter-tabs li a span.enabling {
   color: green;
 }
-#module-filter-left ul li a span.disabling {
+#module-filter-tabs li a span.disabling {
   color: red;
-  margin-left: 5px;
 }
-#module-filter-left ul li a span.counts {
-  font-weight: normal;
+#module-filter-tabs li strong {
+  font-size: 0.923em;
+}
+#module-filter-tabs li a:hover,
+#module-filter-tabs li a:focus {
+  outline: 1px dotted;
+  background: #d5d5d5;
+  text-decoration: none;
+  outline: 0;
+}
+#module-filter-tabs li.selected a,
+#module-filter-tabs li.selected a:hover,
+#module-filter-tabs li.selected a:focus,
+#module-filter-tabs li.selected a:active {
+  background-color: #fff;
+  margin-right: -1px;
+}
+#module-filter-tabs li .summary {
+  display: block;
+  margin-bottom: 0;
+  color: #666;
+  font-size: 0.846em;
+  padding-top: 0.4em;
+}
+#module-filter-tabs li .summary .count {
   display: none;
-  font-size: 0.8em;
-  color: #333333;
-  padding: 2px 0 0;
 }
-#module-filter-left ul li.active a span.counts {
+#module-filter-tabs li.selected .summary .count {
   display: block;
 }
-#module-filter-submit {
-  margin: 0;
+html.js #module-filter-submit input {
+  margin: 2em 2em 1em;
 }
-#module-filter-submit .form-actions {
+html.js .module-filter-inputs-wrapper {
   text-align: center;
 }
-#module-filter-submit input.form-submit {
-  margin: 1em 0 0;
+html.js .module-filter-inputs-wrapper .form-item {
+  margin: 0;
+  padding: 9px;
 }
-#module-filter-submit.fixed {
-  position: fixed;
-  background-color: #F6F6F6;
-  border: 1px solid #D6DBDE;
-  margin-left: -1px;
-  width: 185px;
+html.js .module-filter-inputs-wrapper label {
+  display: inline;
 }
-#module-filter-submit.fixed-top {
-  top: 0;
+html.js .module-filter-inputs-wrapper input[name="module_filter[name]"] {
+  width: 80%;
 }
-#module-filter-submit.fixed-bottom {
-  bottom: 0;
+html.js #module-filter-show-wrapper {
+  margin-bottom: 1em;
 }
-#module-filter-right {
-  display: block;
+html.js #module-filter-modules {
+  margin-left: 240px;
+  border: 1px solid #ccc;
 }
-#module-filter-squeeze {
-  margin-left: 186px;
-  background-color: #FFFFFF;
-  border: 1px solid #D6DBDE;
-  height: auto !important;
+#module-filter-modules table {
+  border-top: none;
+  border-right: none;
+  border-left: none;
 }
-.form-item-module-filter-name {
-  text-align: center;
+html.js #module-filter-modules table {
+  margin: 0;
+  border-bottom: none;
 }
-.form-item-module-filter-name label {
-  display: inline;
+html.js #module-filter-modules table tr,
+html.js #module-filter-modules table td {
+  border-left: 0;
+  border-right: 0;
 }
-#module-filter-show-wrapper .form-checkboxes {
-  text-align: center;
+#module-filter-modules table thead {
+  display: none;
 }
-#module-filter-show-wrapper .form-item:after {
-  display: inline;
+#module-filter-modules table tr.admin-package-title,
+#module-filter-modules table tr.admin-package-title td {
+  border: none !important;
+  border-top: 1px solid #ccc !important;
+  background-color: transparent !important;
+  padding: 10px 0 0;
 }
-table.package {
-  margin: 1em 0;
+#module-filter-modules table tr.admin-package-title.first,
+#module-filter-modules table tr.admin-package-title.first td {
+  border-top: none !important;
 }
-table.package,
-table.package thead,
-table.package tbody,
-table.package tbody tr,
-table.package td:last-child {
-  border-right: 0 none;
-  border-left: 0 none;
+#module-filter-modules table tr.admin-package-header td {
+  border: 1px solid #ccc;
+  text-transform: uppercase;
+  background: #E1E2DC;
+  font-weight: normal;
+  padding: 3px 10px;
 }
-table.package tr.enabling {
+#module-filter-modules table tr.enabling {
   background-color: #dfd;
 }
-table.package tr.disabling {
+#module-filter-modules table tr.disabling {
   background-color: #fcc;
 }
+#module-filter-modules span.module-machine-name {
+  font-size: 0.9em;
+  font-weight: normal;
+}
+.admin-version {
+  white-space: nowrap;
+}
+.admin-operations a.module-link {
+  display: inline;
+}
+
+html.js .toggle-enable {
+  margin: auto;
+  position: relative;
+  width: 50px;
+  overflow: hidden;
+  height: 18px;
+  line-height: 18px;
+  font-size: 11px;
+  text-align: center;
+  cursor: pointer;
+  border: 1px solid #ccc;
+  -moz-border-radius: 3px;
+  -webkit-border-radius: 3px;
+  -khtml-border-radius: 3px;
+  border-radius: 3px;
+  -moz-box-shadow: 0 0 10px rgba(0,0,0,0.50) inset;
+  -webkit-box-shadow: 0 0 10px rgba(0,0,0,0.50) inset;
+  box-shadow: 0 0 10px rgba(0,0,0,0.50) inset;
+  background-clip: padding-box;
+  background-image: -webkit-gradient(linear, 0% 0%, 100% 0%, color-stop(50%, red), color-stop(50%, orange), color-stop(100%, orange));
+  background-image: -moz-linear-gradient(left, red 50%, orange 50%, orange 100%);
+  background-image: linear-gradient(left, red 50%, orange 50%, orange 100%);
+}
+html.js .toggle-enable.enabled {
+  background-image: -webkit-gradient(linear, 0% 0%, 100% 0%, color-stop(50%, orange), color-stop(50%, green), color-stop(100%, green));
+  background-image: -moz-linear-gradient(left, orange 50%, green 50%, green 100%);
+  background-image: linear-gradient(left, orange 50%, green 50%, green 100%);
+}
+html.js .toggle-enable.disabled {
+  background: #ccc;
+  border-color: #ddd;
+  cursor: auto;
+}
+html.js .toggle-enable div {
+  position: relative;
+  color: #777;
+  width: 26px;
+  -moz-border-radius: 2px;
+  -webkit-border-radius: 2px;
+  -khtml-border-radius: 2px;
+  border-radius: 2px;
+  background: white;
+  text-shadow: 1px 1px 0 white;
+  background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(0%, #FEFEFE), color-stop(100%, #EAEAEA));
+  background-image: -moz-linear-gradient(top, #FEFEFE 0%, #EAEAEA 100%);
+  background-image: linear-gradient(top, #FEFEFE 0%, #EAEAEA 100%);
+  -webkit-transition: left 0.2s;
+  -mox-transition: left 0.2s;
+  -o-transition: left 0.2s;
+  transition: left 0.2s;
+}
+html.js .toggle-enable.disabled div {
+  background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(0%, #FEFEFE), color-stop(100%, #EEEEEE));
+  background-image: -moz-linear-gradient(top, #FEFEFE 0%, #EEEEEE 100%);
+  background-image: linear-gradient(top, #FEFEFE 0%, #EEEEEE 100%);
+}
+html.js .toggle-enable div:after,
+html.js .toggle-enable div:before {
+  color: white;
+  text-shadow: none;
+  width: 25px;
+  position: absolute;
+  top: 0;
+  font-size: 9px;
+  font-weight: bold;
+}
+html.js .toggle-enable div:before {
+  content: "OFF";
+  left: -24px;
+}
+html.js .toggle-enable div:after {
+  content: "ON";
+  right: -24px;
+}
+html.js .toggle-enable.off div {
+  left: 24px;
+}
+
+#module-filter-tabs.top-fixed {
+  position: fixed;
+  top: 0;
+}
+#module-filter-tabs.bottom-fixed {
+  position: fixed;
+  bottom: 0;
+}

+ 53 - 0
sites/all/modules/contrib/admin/module_filter/css/modules.css

@@ -0,0 +1,53 @@
+#system-modules table {
+  table-layout: fixed;
+}
+#system-modules th.checkbox {
+  width: 8%;
+}
+#system-modules th.name {
+  width: 25%;
+}
+#system-modules th.version {
+  width: 10%;
+}
+#system-modules th.links {
+  width: 15%;
+}
+#system-modules td {
+  vertical-align: top;
+}
+#system-modules .expand.inner {
+  background: transparent url(../images/collapsed.png) left 0.6em no-repeat;
+  margin-left: -12px;
+  padding-left: 12px;
+}
+#system-modules .expanded.expand.inner {
+  background: transparent url(../images/expanded.png) left 0.6em no-repeat;
+}
+#system-modules .description {
+  cursor: pointer;
+}
+#system-modules .description .inner {
+  overflow: hidden; /* truncates descriptions if too long */
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+#system-modules .description .inner.expanded > * {
+  cursor: auto;
+}
+#system-modules .description .requirements,
+#system-modules .description .links {
+  display: none;
+}
+#system-modules .description .expanded.inner {
+  overflow: visible;
+  white-space: normal;
+}
+#system-modules .description .expanded .requirements,
+#system-modules .description .expanded .links {
+  display: block;
+}
+#system-modules .requirements {
+  padding: 5px 0;
+  max-width: 490px;
+}

+ 18 - 0
sites/all/modules/contrib/admin/module_filter/css/update_status.css

@@ -0,0 +1,18 @@
+#module-filter-update-status-form {
+  float: right;
+}
+.module-filter-inputs-wrapper label {
+  display: inline;
+}
+.module-filter-inputs-wrapper .form-item-module-filter-name {
+  margin-bottom: 0;
+  padding-bottom: 0;
+  text-align: right;
+}
+#module-filter-show-wrapper .form-item {
+  padding: 5px;
+}
+p.module-filter-no-results {
+  clear: both;
+  padding-top: 30px;
+}

BIN
sites/all/modules/contrib/admin/module_filter/images/collapsed.png


BIN
sites/all/modules/contrib/admin/module_filter/images/expanded.png


+ 36 - 21
sites/all/modules/contrib/admin/module_filter/js/dynamic_position.js

@@ -1,33 +1,48 @@
 (function($) {
-  Drupal.behaviors.moduleFilterDynamicPosition = {
-    attach: function() {
-      $(window).scroll(function() {
+
+Drupal.behaviors.moduleFilterDynamicPosition = {
+  attach: function(context) {
+    var $window = $(window);
+
+    $('#module-filter-wrapper', context).once('dynamic-position', function() {
+      // Move the submit button just below the tabs.
+      $('#module-filter-tabs').append($('#module-filter-submit'));
+
+      var positionSubmit = function() {
+        var $tabs = $('#module-filter-tabs');
+        var $submit = $('#module-filter-submit', $tabs);
+
         // Vertical movement.
-        var top = $('#module-filter-tabs').offset().top;
-        var bottom = top + $('#module-filter-tabs').height();
-        var windowHeight = $(window).height();
-        if (((bottom - windowHeight) > ($(window).scrollTop() - $('#module-filter-submit').height())) && $(window).scrollTop() + windowHeight - $('#module-filter-submit').height() - $('#all-tab').height() > top) {
-          $('#module-filter-submit').removeClass('fixed-top').addClass('fixed fixed-bottom');
+        var bottom = $tabs.offset().top + $tabs.outerHeight();
+        if ($submit.hasClass('fixed-bottom')) {
+          bottom += $submit.height();
         }
-        else if (bottom < $(window).scrollTop()) {
-          $('#module-filter-submit').removeClass('fixed-bottom').addClass('fixed fixed-top');
+        if (bottom >= $window.height() + $window.scrollTop()) {
+          $submit.addClass('fixed fixed-bottom');
+          $tabs.css('padding-bottom', $submit.height());
         }
         else {
-          $('#module-filter-submit').removeClass('fixed fixed-bottom fixed-top');
+          $submit.removeClass('fixed fixed-bottom');
+          $tabs.css('padding-bottom', 0);
         }
 
         // Horizontal movement.
-        if ($('#module-filter-submit').hasClass('fixed-bottom') || $('#module-filter-submit').hasClass('fixed-top')) {
-          var left = $('#module-filter-tabs').offset().left - $(window).scrollLeft();
-          if (left != $('#module-filter-submit').offset().left - $(window).scrollLeft()) {
-            $('#module-filter-submit').css('left', left);
+        if ($submit.hasClass('fixed-bottom') || $submit.hasClass('fixed-top')) {
+          var left = $tabs.offset().left - $window.scrollLeft();
+          if (left != $submit.offset().left - $window.scrollLeft()) {
+            $submit.css('left', left);
           }
         }
-      });
-      $(window).trigger('scroll');
-      $(window).resize(function() {
-        $(window).trigger('scroll');
-      });
-    }
+      };
+
+      // Control the positioning.
+      $window.scroll(positionSubmit);
+      $window.resize(positionSubmit);
+      var moduleFilter = $('input[name="module_filter[name]"]').data('moduleFilter');
+      moduleFilter.element.bind('moduleFilter:adjustHeight', positionSubmit);
+      moduleFilter.adjustHeight();
+    });
   }
+};
+
 })(jQuery);

+ 248 - 94
sites/all/modules/contrib/admin/module_filter/js/module_filter.js

@@ -1,120 +1,274 @@
-
 (function ($) {
-  var moduleFilterTimeOut;
-  var moduleFilterTextFilter = '';
-
-  Drupal.behaviors.moduleFilter = {
-    attach: function() {
-      $("#module-filter-wrapper").show();
-      $('input[name="module_filter[name]"]').focus();
-      $('input[name="module_filter[name]"]').keyup(function(e) {
-        switch (e.which) {
-          case 13:
-            if (moduleFilterTimeOut) {
-              clearTimeout(moduleFilterTimeOut);
-            }
-
-            moduleFilter(moduleFilterTextFilter);
-            break;
-          default:
-            if (moduleFilterTextFilter != $(this).val()) {
-              moduleFilterTextFilter = this.value;
-              if (moduleFilterTimeOut) {
-                clearTimeout(moduleFilterTimeOut);
-              }
-
-              moduleFilterTimeOut = setTimeout('moduleFilter("' + moduleFilterTextFilter + '")', 500);
-            }
-            break;
+
+Drupal.ModuleFilter = {};
+
+Drupal.ModuleFilter.explode = function(string) {
+  var queryArray = string.match(/([a-zA-Z]+\:(\w+|"[^"]+")*)|\w+|"[^"]+"/g);
+  if (!queryArray) {
+    queryArray = new Array();
+  }
+  var i = queryArray.length;
+  while (i--) {
+    queryArray[i] = queryArray[i].replace(/"/g, "");
+  }
+  return queryArray;
+};
+
+Drupal.ModuleFilter.getState = function(key) {
+  if (!Drupal.ModuleFilter.state) {
+    Drupal.ModuleFilter.state = {};
+    var cookie = $.cookie('DrupalModuleFilter');
+    var query = cookie ? cookie.split('&') : [];
+    if (query) {
+      for (var i in query) {
+        // Extra check to avoid js errors in Chrome, IE and Safari when
+        // combined with JS like twitter's widget.js.
+        // See http://drupal.org/node/798764.
+        if (typeof(query[i]) == 'string' && query[i].indexOf('=') != -1) {
+          var values = query[i].split('=');
+          if (values.length === 2) {
+            Drupal.ModuleFilter.state[values[0]] = values[1];
+          }
         }
-      });
-      $('input[name="module_filter[name]"]').keypress(function(e) {
-        if (e.which == 13) e.preventDefault();
-      });
-
-      $('#edit-module-filter-show-enabled').change(function() {
-        moduleFilter($('input[name="module_filter[name]"]').val());
-      });
-      $('#edit-module-filter-show-disabled').change(function() {
-        moduleFilter($('input[name="module_filter[name]"]').val());
-      });
-      $('#edit-module-filter-show-required').change(function() {
-        moduleFilter($('input[name="module_filter[name]"]').val());
-      });
-      $('#edit-module-filter-show-unavailable').change(function() {
-        moduleFilter($('input[name="module_filter[name]"]').val());
-      });
+      }
+    }
+  }
+  return Drupal.ModuleFilter.state[key] ? Drupal.ModuleFilter.state[key] : false;
+};
+
+Drupal.ModuleFilter.setState = function(key, value) {
+  var existing = Drupal.ModuleFilter.getState(key);
+  if (existing != value) {
+    Drupal.ModuleFilter.state[key] = value;
+    var query = [];
+    for (var i in Drupal.ModuleFilter.state) {
+      query.push(i + '=' + Drupal.ModuleFilter.state[i]);
     }
+    $.cookie('DrupalModuleFilter', query.join('&'), { expires: 7, path: '/' });
+  }
+};
+
+Drupal.ModuleFilter.Filter = function(element, selector, options) {
+  var self = this;
+
+  this.element = element;
+  this.text = $(this.element).val();
+
+  this.settings = Drupal.settings.moduleFilter;
+
+  this.selector = selector;
+
+  this.options = $.extend({
+    delay: 500,
+    striping: false,
+    childSelector: null,
+    empty: Drupal.t('No results'),
+    rules: new Array()
+  }, options);
+  if (this.options.wrapper == undefined) {
+    this.options.wrapper = $(self.selector).parent();
+  }
+
+  // Add clear button.
+  this.element.after('<div class="module-filter-clear"><a href="#" class="js-hide">' + Drupal.t('clear') + '</a></div>');
+  if (this.text) {
+    $('.module-filter-clear a', this.element.parent()).removeClass('js-hide');
   }
+  $('.module-filter-clear a', this.element.parent()).click(function() {
+    self.element.val('');
+    self.text = '';
+    delete self.queries;
+    self.applyFilter();
+    self.element.focus();
+    $(this).addClass('js-hide');
+    return false;
+  });
 
-  moduleFilter = function(string) {
-    stringLowerCase = string.toLowerCase();
+  this.updateQueries = function() {
+    var queryStrings = Drupal.ModuleFilter.explode(self.text);
 
-    $("fieldset table tbody tr td label > strong").each(function(i) {
-      var $row = $(this).parents('tr');
-      var module = $(this).text();
-      var moduleLowerCase = module.toLowerCase();
-      var $fieldset = $row.parents('fieldset');
+    self.queries = new Array();
+    for (var i in queryStrings) {
+      var query = { operator: 'text', string: queryStrings[i] };
 
-      if (string != '') {
-        if ($fieldset.hasClass('collapsed')) {
-          $fieldset.removeClass('collapsed');
+      if (self.operators != undefined) {
+        // Check if an operator is possibly used.
+        if (queryStrings[i].indexOf(':') > 0) {
+          // Determine operator used.
+          var args = queryStrings[i].split(':', 2);
+          var operator = args.shift();
+          if (self.operators[operator] != undefined) {
+            query.operator = operator;
+            query.string = args.shift();
+          }
         }
       }
 
-      if (moduleLowerCase.match(stringLowerCase)) {
-        if (moduleFilterVisible($('td.checkbox input', $row))) {
-          if (!$row.is(':visible')) {
-            $row.show();
-            if ($fieldset.hasClass('collapsed')) {
-              $fieldset.removeClass('collapsed');
-            }
-            if (!$fieldset.is(':visible')) {
-              $fieldset.show();
-            }
+      query.string = query.string.toLowerCase();
+
+      self.queries.push(query);
+    }
+
+    if (self.queries.length <= 0) {
+      // Add a blank string query.
+      self.queries.push({ operator: 'text', string: '' });
+    }
+  };
+
+  this.applyFilter = function() {
+    self.results = new Array();
+
+    self.updateQueries();
+
+    if (self.index == undefined) {
+      self.buildIndex();
+    }
+
+    self.element.trigger('moduleFilter:start');
+
+    $.each(self.index, function(key, item) {
+      var $item = item.element;
+
+      for (var i in self.queries) {
+        var query = self.queries[i];
+        if (query.operator == 'text') {
+          if (item.text.indexOf(query.string) < 0) {
+            continue;
           }
         }
         else {
-          $row.hide();
-          if ($row.siblings(':visible').html() == null) {
-            $fieldset.hide();
+          var func = self.operators[query.operator];
+          if (!(func(query.string, self, item))) {
+            continue;
           }
         }
-      }
-      else {
-        if ($row.is(':visible')) {
-          $row.hide();
-          if ($row.siblings(':visible').html() == null) {
-            $fieldset.hide();
-          }
-        }
-      }
-    });
-  }
 
-  moduleFilterVisible = function(checkbox) {
-    if (checkbox.length > 0) {
-      if ($('#edit-module-filter-show-enabled').is(':checked')) {
-        if ($(checkbox).is(':checked') && !$(checkbox).is(':disabled')) {
+        var rulesResult = self.processRules(item);
+        if (rulesResult !== false) {
           return true;
         }
       }
-      if ($('#edit-module-filter-show-disabled').is(':checked')) {
-        if (!$(checkbox).is(':checked') && !$(checkbox).is(':disabled')) {
-          return true;
+
+      $item.addClass('js-hide');
+    });
+
+    self.element.trigger('moduleFilter:finish', { results: self.results });
+
+    if (self.options.striping) {
+      self.stripe();
+    }
+
+    if (self.results.length > 0) {
+      self.options.wrapper.find('.module-filter-no-results').remove();
+    }
+    else {
+      if (!self.options.wrapper.find('.module-filter-no-results').length) {
+        self.options.wrapper.append($('<p class="module-filter-no-results"/>').text(self.options.empty));
+      };
+    }
+  };
+
+  self.element.keyup(function(e) {
+    switch (e.which) {
+      case 13:
+        if (self.timeOut) {
+          clearTimeout(self.timeOut);
         }
-      }
-      if ($('#edit-module-filter-show-required').is(':checked')) {
-        if ($(checkbox).is(':checked') && $(checkbox).is(':disabled')) {
-          return true;
+        self.applyFilter();
+        break;
+      default:
+        if (self.text != $(this).val()) {
+          if (self.timeOut) {
+            clearTimeout(self.timeOut);
+          }
+
+          self.text = $(this).val();
+
+          if (self.text) {
+            self.element.parent().find('.module-filter-clear a').removeClass('js-hide');
+          }
+          else {
+            self.element.parent().find('.module-filter-clear a').addClass('js-hide');
+          }
+
+          self.element.trigger('moduleFilter:keyup');
+
+          self.timeOut = setTimeout(self.applyFilter, self.options.delay);
         }
-      }
+        break;
     }
-    if ($('#edit-module-filter-show-unavailable').is(':checked')) {
-      if (checkbox.length == 0 || ($(checkbox).size() > 0 && !$(checkbox).is(':checked') && $(checkbox).is(':disabled'))) {
-        return true;
+  });
+
+  self.element.keypress(function(e) {
+    if (e.which == 13) e.preventDefault();
+  });
+};
+
+Drupal.ModuleFilter.Filter.prototype.buildIndex = function() {
+  var self = this;
+  var index = new Array();
+  $(this.selector).each(function(i) {
+    var text = (self.options.childSelector) ? $(self.options.childSelector, this).text() : $(this).text();
+    var item = {
+      key: i,
+      element: $(this),
+      text: text.toLowerCase()
+    };
+    for (var j in self.options.buildIndex) {
+      var func = self.options.buildIndex[j];
+      item = $.extend(func(self, item), item);
+    }
+    $(this).data('indexKey', i);
+    index.push(item);
+    delete item;
+  });
+  this.index = index;
+};
+
+Drupal.ModuleFilter.Filter.prototype.processRules = function(item) {
+  var self = this;
+  var $item = item.element;
+  var rulesResult = true;
+  if (self.options.rules.length > 0) {
+    for (var i in self.options.rules) {
+      var func = self.options.rules[i];
+      rulesResult = func(self, item);
+      if (rulesResult === false) {
+        break;
       }
     }
-    return false;
   }
+  if (rulesResult !== false) {
+    $item.removeClass('js-hide');
+    self.results.push(item);
+  }
+  return rulesResult;
+};
+
+Drupal.ModuleFilter.Filter.prototype.stripe = function() {
+  var self = this;
+  var flip = { even: 'odd', odd: 'even' };
+  var stripe = 'odd';
+
+  $.each(self.index, function(key, item) {
+    if (!item.element.hasClass('js-hide')) {
+      item.element.removeClass('odd even')
+        .addClass(stripe);
+      stripe = flip[stripe];
+    }
+  });
+};
+
+$.fn.moduleFilter = function(selector, options) {
+  var filterInput = this;
+  filterInput.parents('.module-filter-inputs-wrapper').show();
+  if (Drupal.settings.moduleFilter.setFocus) {
+    filterInput.focus();
+  }
+  if (Drupal.settings.moduleFilter.expandedDescription) {
+    $('#system-modules td.description .inner.expand').addClass('expanded');
+  }
+  filterInput.data('moduleFilter', new Drupal.ModuleFilter.Filter(this, selector, options));
+};
+
 })(jQuery);

+ 505 - 227
sites/all/modules/contrib/admin/module_filter/js/module_filter_tab.js

@@ -1,281 +1,559 @@
 
 (function ($) {
-  Drupal.ModuleFilter = Drupal.ModuleFilter || {};
-  Drupal.ModuleFilter.textFilter = '';
-  Drupal.ModuleFilter.timeout;
-  Drupal.ModuleFilter.tabs = {};
-  Drupal.ModuleFilter.enabling = {};
-  Drupal.ModuleFilter.disabling = {};
-
-  Drupal.behaviors.moduleFilter = {
-    attach: function() {
-      // Set the focus on the module filter textfield.
-      $('input[name="module_filter[name]"]').focus();
-
-      $('#module-filter-squeeze').css('min-height', $('#module-filter-tabs').height());
-
-      $('#module-filter-left a.project-tab').each(function(i) {
-        Drupal.ModuleFilter.tabs[$(this).attr('id')] = new Drupal.ModuleFilter.Tab(this);
-      });
 
-      // Move anchors to top of tabs.
-      $('a.anchor', $('#module-filter-left')).remove().prependTo('#module-filter-tabs');
+Drupal.ModuleFilter.tabs = {};
+Drupal.ModuleFilter.enabling = {};
+Drupal.ModuleFilter.disabling = {};
+
+Drupal.ModuleFilter.jQueryIsNewer = function() {
+  if (Drupal.ModuleFilter.jQueryNewer == undefined) {
+    var v1parts = $.fn.jquery.split('.');
+    var v2parts = new Array('1', '4', '4');
+
+    for (var i = 0; i < v1parts.length; ++i) {
+      if (v2parts.length == i) {
+        Drupal.ModuleFilter.jQueryNewer = true;
+        return Drupal.ModuleFilter.jQueryNewer;
+      }
+
+      if (v1parts[i] == v2parts[i]) {
+        continue;
+      }
+      else if (v1parts[i] > v2parts[i]) {
+        Drupal.ModuleFilter.jQueryNewer = true;
+        return Drupal.ModuleFilter.jQueryNewer;
+      }
+      else {
+        Drupal.ModuleFilter.jQueryNewer = false;
+        return Drupal.ModuleFilter.jQueryNewer;
+      }
+    }
+
+    if (v1parts.length != v2parts.length) {
+      Drupal.ModuleFilter.jQueryNewer = false;
+      return Drupal.ModuleFilter.jQueryNewer;
+    }
+
+    Drupal.ModuleFilter.jQueryNewer = false;
+  }
+  return Drupal.ModuleFilter.jQueryNewer;
+};
+
+Drupal.behaviors.moduleFilterTabs = {
+  attach: function(context) {
+    if (Drupal.settings.moduleFilter.tabs) {
+      $('#module-filter-wrapper table:not(.sticky-header)', context).once('module-filter-tabs', function() {
+        var $modules = $('#module-filter-modules');
+        var moduleFilter = $('input[name="module_filter[name]"]').data('moduleFilter');
+        var table = $(this);
+
+        $('thead', table).show();
+
+        // Remove package header rows.
+        $('tr.admin-package-header', table).remove();
+
+        var $tabsWrapper = $('<div id="module-filter-tabs"></div>');
+
+        // Build tabs from package title rows.
+        var tabs = '<ul>';
+        for (var i in Drupal.settings.moduleFilter.packageIDs) {
+          var id = Drupal.checkPlain(Drupal.settings.moduleFilter.packageIDs[i]);
+
+          var name = id;
+          var tabClass = 'project-tab';
+          var title = null;
+          var summary = (Drupal.settings.moduleFilter.countEnabled) ? '<span class="count">' + Drupal.ModuleFilter.countSummary(id) + '</span>' : '';
+
+          switch (id) {
+            case 'all':
+              name = Drupal.t('All');
+              break;
+            case 'new':
+              name = Drupal.t('New');
+              title = Drupal.t('Modules installed within the last week.');
+              if (Drupal.settings.moduleFilter.enabledCounts['new'].total == 0) {
+                tabClass += ' disabled';
+                summary += '<span>' + Drupal.t('No modules added within the last week.') + '</span>';
+              }
+              break;
+            case 'recent':
+              name = Drupal.t('Recent');
+              title = Drupal.t('Modules enabled/disabled within the last week.');
+              if (Drupal.settings.moduleFilter.enabledCounts['recent'].total == 0) {
+                tabClass += ' disabled';
+                summary += '<span>' + Drupal.t('No modules were enabled or disabled within the last week.') + '</span>';
+              }
+              break;
+            default:
+              var $row = $('#' + id + '-package', this);
+              name = Drupal.checkPlain($.trim($row.text()));
+              $row.remove();
+              break;
+          }
 
-      $('input[name="module_filter[name]"]').keyup(function(e) {
-        switch (e.which) {
-          case 13:
-            if (Drupal.ModuleFilter.timeout) {
-              clearTimeout(Drupal.ModuleFilter.timeout);
+          tabs += '<li id="' + id + '-tab" class="' + tabClass + '"><a href="#' + id + '" class="overlay-exclude"' + (title ? ' title="' + title + '"' : '') + '><strong>' + name + '</strong><span class="summary">' + summary + '</span></a></li>';
+        }
+        tabs += '</ul>';
+        $tabsWrapper.append(tabs);
+        $modules.before($tabsWrapper);
+
+        // Index tabs.
+        $('#module-filter-tabs li').each(function() {
+          var $tab = $(this);
+          var id = $tab.attr('id');
+          Drupal.ModuleFilter.tabs[id] = new Drupal.ModuleFilter.Tab($tab, id);
+        });
+
+        $('tbody td.checkbox input', $modules).change(function() {
+          var $checkbox = $(this);
+          var key = $checkbox.parents('tr').data('indexKey');
+
+          moduleFilter.index[key].status = $checkbox.is(':checked');
+
+          if (Drupal.settings.moduleFilter.visualAid) {
+            var type = ($checkbox.is(':checked')) ? 'enable' : 'disable';
+            Drupal.ModuleFilter.updateVisualAid(type, $checkbox.parents('tr'));
+          }
+        });
+
+        // Sort rows.
+        var rows = $('tbody tr.module', table).get();
+        rows.sort(function(a, b) {
+          var compA = $('td:nth(1)', a).text().toLowerCase();
+          var compB = $('td:nth(1)', b).text().toLowerCase();
+          return (compA < compB) ? -1 : (compA > compB) ? 1 : 0;
+        });
+        $.each(rows, function(idx, itm) { table.append(itm); });
+
+        // Re-stripe rows.
+        $('tr.module', table)
+          .removeClass('odd even')
+          .filter(':odd').addClass('even').end()
+          .filter(':even').addClass('odd');
+
+        moduleFilter.adjustHeight();
+
+        moduleFilter.element.bind('moduleFilter:start', function() {
+          moduleFilter.tabResults = {
+            'all-tab': { items: {}, count: 0 },
+            'recent-tab': { items: {}, count: 0 },
+            'new-tab': { items: {}, count: 0 }
+          };
+
+          // Empty result info from tabs.
+          for (var i in Drupal.ModuleFilter.tabs) {
+            if (Drupal.ModuleFilter.tabs[i].resultInfo != undefined) {
+              Drupal.ModuleFilter.tabs[i].resultInfo.empty();
             }
+          }
+        });
+
+        moduleFilter.element.bind('moduleFilter:finish', function(e, data) {
+          $.each(moduleFilter.index, function(key, item) {
+            if (!item.element.hasClass('js-hide')) {
+              var id = Drupal.ModuleFilter.getTabID(item.element);
+
+              if (moduleFilter.tabResults[id] == undefined) {
+                moduleFilter.tabResults[id] = { items: {}, count: 0 };
+              }
+              if (moduleFilter.tabResults[id].items[item.key] == undefined) {
+                // All tab
+                moduleFilter.tabResults['all-tab'].count++;
+
+                // Recent tab
+                if (item.element.hasClass('recent-module')) {
+                  moduleFilter.tabResults['recent-tab'].count++;
+                }
+
+                // New tab
+                if (item.element.hasClass('new-module')) {
+                  moduleFilter.tabResults['new-tab'].count++;
+                }
 
-            Drupal.ModuleFilter.filter(Drupal.ModuleFilter.textFilter);
-            break;
-          default:
-            if (Drupal.ModuleFilter.textFilter != $(this).val()) {
-              Drupal.ModuleFilter.textFilter = this.value;
-              if (Drupal.ModuleFilter.timeout) {
-                clearTimeout(Drupal.ModuleFilter.timeout);
+                moduleFilter.tabResults[id].items[item.key] = item;
+                moduleFilter.tabResults[id].count++;
+              }
+
+              if (Drupal.ModuleFilter.activeTab != undefined && Drupal.ModuleFilter.activeTab.id != 'all-tab') {
+                if ((Drupal.ModuleFilter.activeTab.id == 'recent-tab' && !item.element.hasClass('recent-module')) || (Drupal.ModuleFilter.activeTab.id == 'new-tab' && !item.element.hasClass('new-module')) || (Drupal.ModuleFilter.activeTab.id != 'recent-tab' && Drupal.ModuleFilter.activeTab.id != 'new-tab' && id != Drupal.ModuleFilter.activeTab.id)) {
+                  // The item is not in the active tab, so hide it.
+                  item.element.addClass('js-hide');
+                }
               }
-              Drupal.ModuleFilter.timeout = setTimeout('Drupal.ModuleFilter.filter("' + Drupal.ModuleFilter.textFilter + '")', 500);
             }
-            break;
-        }
-      });
-      $('input[name="module_filter[name]"]').keypress(function(e) {
-        if (e.which == 13) e.preventDefault();
-      });
+          });
 
-      Drupal.ModuleFilter.showEnabled = $('#edit-module-filter-show-enabled').is(':checked');
-      $('#edit-module-filter-show-enabled').change(function() {
-        Drupal.ModuleFilter.showEnabled = $(this).is(':checked');
-        Drupal.ModuleFilter.filter($('input[name="module_filter[name]"]').val());
-      });
-      Drupal.ModuleFilter.showDisabled = $('#edit-module-filter-show-disabled').is(':checked');
-      $('#edit-module-filter-show-disabled').change(function() {
-        Drupal.ModuleFilter.showDisabled = $(this).is(':checked');
-        Drupal.ModuleFilter.filter($('input[name="module_filter[name]"]').val());
-      });
-      Drupal.ModuleFilter.showRequired = $('#edit-module-filter-show-required').is(':checked');
-      $('#edit-module-filter-show-required').change(function() {
-        Drupal.ModuleFilter.showRequired = $(this).is(':checked');
-        Drupal.ModuleFilter.filter($('input[name="module_filter[name]"]').val());
-      });
-      Drupal.ModuleFilter.showUnavailable = $('#edit-module-filter-show-unavailable').is(':checked');
-      $('#edit-module-filter-show-unavailable').change(function() {
-        Drupal.ModuleFilter.showUnavailable = $(this).is(':checked');
-        Drupal.ModuleFilter.filter($('input[name="module_filter[name]"]').val());
-      });
+          if (Drupal.settings.moduleFilter.visualAid) {
+            if (moduleFilter.text) {
+              // Add result info to tabs.
+              for (var id in moduleFilter.tabResults) {
+                var tab = Drupal.ModuleFilter.tabs[id];
+
+                if (tab.resultInfo == undefined) {
+                  var resultInfo = '<span class="result-info"></span>'
+                  $('a', tab.element).prepend(resultInfo);
+                  tab.resultInfo = $('span.result-info', tab.element);
+                }
 
-      if (Drupal.settings.moduleFilter.visualAid == 1) {
-        $('table.package tbody td.checkbox input').change(function() {
-          if ($(this).is(':checked')) {
-            Drupal.ModuleFilter.updateVisualAid('enable', $(this).parents('tr'));
+                tab.resultInfo.append(moduleFilter.tabResults[id].count);
+              }
+
+              if (Drupal.settings.moduleFilter.hideEmptyTabs) {
+                for (var id in Drupal.ModuleFilter.tabs) {
+                  if (moduleFilter.tabResults[id] != undefined) {
+                    Drupal.ModuleFilter.tabs[id].element.show();
+                  }
+                  else if (Drupal.ModuleFilter.activeTab == undefined || Drupal.ModuleFilter.activeTab.id != id) {
+                    Drupal.ModuleFilter.tabs[id].element.hide();
+                  }
+                }
+              }
+            }
+            else {
+              // Make sure all tabs are visible.
+              if (Drupal.settings.moduleFilter.hideEmptyTabs) {
+                $('#module-filter-tabs li').show();
+              }
+            }
           }
-          else {
-            Drupal.ModuleFilter.updateVisualAid('disable', $(this).parents('tr'));
+
+          if ((Drupal.ModuleFilter.activeTab != undefined && (moduleFilter.tabResults[Drupal.ModuleFilter.activeTab.id] == undefined || moduleFilter.tabResults[Drupal.ModuleFilter.activeTab.id].count <= 0))) {
+            // The current tab contains no results.
+            moduleFilter.results = 0;
           }
+
+          moduleFilter.adjustHeight();
         });
-      }
 
-      // Check for anchor.
-      var url = document.location.toString();
-      if (url.match('#')) {
-        // Make tab active based on anchor.
-        var anchor = '#' + url.split('#')[1];
-        $('a[href="' + anchor + '"]').click();
-      }
-      // Else if no active tab is defined, set it to the all tab.
-      else if (Drupal.ModuleFilter.activeTab == undefined) {
-        Drupal.ModuleFilter.activeTab = Drupal.ModuleFilter.tabs['all-tab'];
-      }
-    }
-  }
+        if (Drupal.settings.moduleFilter.useURLFragment) {
+          $(window).bind('hashchange.module-filter', $.proxy(Drupal.ModuleFilter, 'eventHandlerOperateByURLFragment')).triggerHandler('hashchange.module-filter');
+        }
+        else {
+          Drupal.ModuleFilter.selectTab();
+        }
 
-  Drupal.ModuleFilter.visible = function(checkbox) {
-    if (checkbox.length > 0) {
-      if (Drupal.ModuleFilter.showEnabled) {
-        if ($(checkbox).is(':checked') && !$(checkbox).is(':disabled')) {
-          return true;
+        if (Drupal.settings.moduleFilter.useSwitch) {
+          $('td.checkbox div.form-item', table).hide();
+          $('td.checkbox', table).each(function(i) {
+            var $cell = $(this);
+            var $checkbox = $(':checkbox', $cell);
+            var $switch = $('.toggle-enable', $cell);
+            $switch.removeClass('js-hide').click(function() {
+              if (!$(this).hasClass('disabled')) {
+                if (Drupal.ModuleFilter.jQueryIsNewer()) {
+                  $checkbox.click();
+                  $switch.toggleClass('off');
+                }
+                else {
+                  $checkbox.click().change();
+                  $switch.toggleClass('off');
+                }
+              }
+            });
+          });
         }
-      }
-      if (Drupal.ModuleFilter.showDisabled) {
-        if (!$(checkbox).is(':checked') && !$(checkbox).is(':disabled')) {
-          return true;
+
+        var $tabs = $('#module-filter-tabs');
+
+        function getParentTopOffset($obj, offset) {
+          var $parent = $obj.offsetParent();
+          if ($obj[0] != $parent[0]) {
+            offset += $parent.position().top;
+            return getParentTopOffset($parent, offset);
+          }
+          return offset;
         }
-      }
-      if (Drupal.ModuleFilter.showRequired) {
-        if ($(checkbox).is(':checked') && $(checkbox).is(':disabled')) {
-          return true;
+
+        var tabsTopOffset = null;
+        function getParentsTopOffset() {
+          if (tabsTopOffset === null) {
+            tabsTopOffset = getParentTopOffset($tabs.parent(), 0);
+          }
+          return tabsTopOffset;
+        }
+
+        function viewportTop() {
+          var top = $(window).scrollTop();
+          return top;
         }
-      }
-    }
-    if (Drupal.ModuleFilter.showUnavailable) {
-      if (checkbox.length == 0 || (!$(checkbox).is(':checked') && $(checkbox).is(':disabled'))) {
-        return true;
-      }
-    }
-    return false;
-  }
 
-  Drupal.ModuleFilter.filter = function(string) {
-    var stringLowerCase = string.toLowerCase();
-    var flip = 'odd';
+        function viewportBottom() {
+          var top = $(window).scrollTop();
+          var bottom = top + $(window).height();
 
-    if (Drupal.ModuleFilter.activeTab.id == 'all-tab') {
-      var selector = 'table.package tbody tr td label > strong';
-    }
-    else {
-      var selector = 'table.package tbody tr.' + Drupal.ModuleFilter.activeTab.id + '-content td label > strong';
-    }
+          bottom -= $('#page-actions').height();
 
-    $(selector).each(function(i) {
-      var $row = $(this).parents('tr');
-      var module = $(this).text();
-      var moduleLowerCase = module.toLowerCase();
-
-      if (moduleLowerCase.match(stringLowerCase)) {
-        if (Drupal.ModuleFilter.visible($('td.checkbox :checkbox', $row))) {
-          $row.removeClass('odd even');
-          $row.addClass(flip);
-          $row.show();
-          flip = (flip == 'odd') ? 'even' : 'odd';
+          return bottom;
         }
-        else {
-          $row.hide();
+
+        function fixToTop(top) {
+          if ($tabs.hasClass('bottom-fixed')) {
+            $tabs.css({
+              'position': 'absolute',
+              'top': $tabs.position().top - getParentsTopOffset(),
+              'bottom': 'auto'
+            });
+            $tabs.removeClass('bottom-fixed');
+          }
+
+          if (($tabs.css('position') == 'absolute' && $tabs.offset().top - top >= 0) || ($tabs.css('position') != 'absolute' && $tabs.offset().top - top <= 0)) {
+            $tabs.addClass('top-fixed');
+            $tabs.attr('style', '');
+          }
+        }
+
+        function fixToBottom(bottom) {
+          if ($tabs.hasClass('top-fixed')) {
+            $tabs.css({
+              'position': 'absolute',
+              'top': $tabs.position().top - getParentsTopOffset(),
+              'bottom': 'auto'
+            });
+            $tabs.removeClass('top-fixed');
+          }
+
+          if ($tabs.offset().top + $tabs.height() - bottom <= 0) {
+            $tabs.addClass('bottom-fixed');
+            var style = '';
+            var pageActionsHeight = $('#page-actions').height();
+            if (pageActionsHeight > 0) {
+              style = 'bottom: ' + pageActionsHeight + 'px';
+            }
+            else if (Drupal.settings.moduleFilter.dynamicPosition) {
+              // style = 'bottom: ' + $('#module-filter-submit', $tabs).height() + 'px';
+            }
+            $tabs.attr('style', style);
+          }
         }
-      }
-      else {
-        $row.hide();
-      }
-    });
-  }
 
-  Drupal.ModuleFilter.Tab = function(element) {
-    this.id = $(element).attr('id');
-    this.element = element;
+        var lastTop = 0;
+        $(window).scroll(function() {
+          var top = viewportTop();
+          var bottom = viewportBottom();
 
-    $(this.element).click(function() {
-      Drupal.ModuleFilter.tabs[$(this).attr('id')].setActive();
-    });
+          if ($modules.offset().top >= top) {
+            $tabs.removeClass('top-fixed').attr('style', '');
+          }
+          else {
+            if (top > lastTop) { // Downward scroll.
+              if ($tabs.height() > bottom - top) {
+                fixToBottom(bottom);
+              }
+              else {
+                fixToTop(top);
+              }
+            }
+            else { // Upward scroll.
+              fixToTop(top);
+            }
+          }
+          lastTop = top;
+        });
+
+        moduleFilter.adjustHeight();
+      });
+    }
   }
+};
+
+Drupal.ModuleFilter.Tab = function(element, id) {
+  var self = this;
+
+  this.id = id;
+  this.hash = id.substring(0, id.length - 4);
+  this.element = element;
+
+  $('a', this.element).click(function() {
+    if (!Drupal.settings.moduleFilter.useURLFragment) {
+      var hash = (!self.element.hasClass('selected')) ? self.hash : 'all';
+      Drupal.ModuleFilter.selectTab(hash);
+      return false;
+    }
 
-  Drupal.ModuleFilter.Tab.prototype.setActive = function() {
-    if (Drupal.ModuleFilter.activeTab) {
-      $(Drupal.ModuleFilter.activeTab.element).parent().removeClass('active');
+    if (self.element.hasClass('selected')) {
+      // Clear the active tab.
+      window.location.hash = 'all';
+      return false;
     }
-    // Assume the default active tab is #all-tab. Remove its active class.
-    else {
-      $('#all-tab').parent().removeClass('active');
+  });
+
+  $('tr.' + this.id, $('#system-modules')).hover(
+    function() {
+      self.element.addClass('suggest');
+    },
+    function() {
+      self.element.removeClass('suggest');
     }
+  );
+};
 
-    Drupal.ModuleFilter.activeTab = this;
-    $(Drupal.ModuleFilter.activeTab.element).parent().addClass('active');
-    Drupal.ModuleFilter.activeTab.displayRows();
+Drupal.ModuleFilter.selectTab = function(hash) {
+  if (!hash || Drupal.ModuleFilter.tabs[hash + '-tab'] == undefined || Drupal.settings.moduleFilter.enabledCounts[hash].total == 0) {
+    if (Drupal.settings.moduleFilter.rememberActiveTab) {
+      var activeTab = Drupal.ModuleFilter.getState('activeTab');
+      if (activeTab && Drupal.ModuleFilter.tabs[activeTab + '-tab'] != undefined) {
+        hash = activeTab;
+      }
+    }
 
-    // Clear filter textfield and refocus on it.
-    $('input[name="module_filter[name]"]').val('');
-    $('input[name="module_filter[name]"]').focus();
+    if (!hash) {
+      hash = 'all';
+    }
   }
 
-  Drupal.ModuleFilter.Tab.prototype.displayRows = function() {
-    var flip = 'odd';
-    var selector = (Drupal.ModuleFilter.activeTab.id == 'all-tab') ? 'table.package tbody tr' : 'table.package tbody tr.' + this.id + '-content';
-    $('table.package tbody tr').hide();
-    $('table.package tbody tr').removeClass('odd even');
-    $(selector).each(function(i) {
-      if (Drupal.ModuleFilter.visible($('td.checkbox input', $(this)))) {
-        $(this).addClass(flip);
-        flip = (flip == 'odd') ? 'even' : 'odd';
-        $(this).show();
-      }
-    });
+  if (Drupal.ModuleFilter.activeTab != undefined) {
+    Drupal.ModuleFilter.activeTab.element.removeClass('selected');
   }
 
-  Drupal.ModuleFilter.Tab.prototype.updateEnabling = function(amount) {
-    this.enabling = this.enabling || 0;
-    this.enabling += amount;
-    if (this.enabling == 0) {
-      delete(this.enabling);
-    }
+  Drupal.ModuleFilter.activeTab = Drupal.ModuleFilter.tabs[hash + '-tab'];
+  Drupal.ModuleFilter.activeTab.element.addClass('selected');
+
+  var moduleFilter = $('input[name="module_filter[name]"]').data('moduleFilter');
+  var filter = moduleFilter.applyFilter();
+
+  if (!Drupal.ModuleFilter.modulesTop) {
+    Drupal.ModuleFilter.modulesTop = $('#module-filter-modules').offset().top;
+  }
+  else {
+    // Calculate header offset; this is important in case the site is using
+    // admin_menu module which has fixed positioning and is on top of everything
+    // else.
+    var headerOffset = Drupal.settings.tableHeaderOffset ? eval(Drupal.settings.tableHeaderOffset + '()') : 0;
+    // Scroll back to top of #module-filter-modules.
+    $('html, body').animate({
+      scrollTop: Drupal.ModuleFilter.modulesTop - headerOffset
+    }, 500);
+    // $('html, body').scrollTop(Drupal.ModuleFilter.modulesTop);
   }
 
-  Drupal.ModuleFilter.Tab.prototype.updateDisabling = function(amount) {
-    this.disabling = this.disabling || 0;
-    this.disabling += amount;
-    if (this.disabling == 0) {
-      delete(this.disabling);
-    }
+  Drupal.ModuleFilter.setState('activeTab', hash);
+};
+
+Drupal.ModuleFilter.eventHandlerOperateByURLFragment = function(event) {
+  var hash = $.param.fragment();
+  Drupal.ModuleFilter.selectTab(hash);
+};
+
+Drupal.ModuleFilter.countSummary = function(id) {
+  return Drupal.t('@enabled of @total', { '@enabled': Drupal.settings.moduleFilter.enabledCounts[id].enabled, '@total': Drupal.settings.moduleFilter.enabledCounts[id].total });
+};
+
+Drupal.ModuleFilter.Tab.prototype.updateEnabling = function(name, remove) {
+  this.enabling = this.enabling || {};
+  if (!remove) {
+    this.enabling[name] = name;
+  }
+  else {
+    delete this.enabling[name];
   }
+};
 
-  Drupal.ModuleFilter.Tab.prototype.updateVisualAid = function() {
-    var visualAid = '';
-    if (this.enabling != undefined) {
-      visualAid += '<span class="enabling">' + Drupal.t('+@count', { '@count': this.enabling }) + '</span>';
+Drupal.ModuleFilter.Tab.prototype.updateDisabling = function(name, remove) {
+  this.disabling = this.disabling || {};
+  if (!remove) {
+    this.disabling[name] = name;
+  }
+  else {
+    delete this.disabling[name];
+  }
+};
+
+Drupal.ModuleFilter.Tab.prototype.updateVisualAid = function() {
+  var visualAid = '';
+  var enabling = new Array();
+  var disabling = new Array();
+
+  if (this.enabling != undefined) {
+    for (var i in this.enabling) {
+      enabling.push(this.enabling[i]);
     }
-    if (this.disabling != undefined) {
-      visualAid += '<span class="disabling">' + Drupal.t('-@count', { '@count': this.disabling }) + '</span>';
+    if (enabling.length > 0) {
+      enabling.sort();
+      visualAid += '<span class="enabling">+' + enabling.join('</span>, <span class="enabling">') + '</span>';
     }
-
-    if (!$('span.visual-aid', $(this.element)).size() && visualAid != '') {
-      $(this.element).prepend('<span class="visual-aid"></span>');
+  }
+  if (this.disabling != undefined) {
+    for (var i in this.disabling) {
+      disabling.push(this.disabling[i]);
+    }
+    if (disabling.length > 0) {
+      disabling.sort();
+      if (enabling.length > 0) {
+        visualAid += '<br />';
+      }
+      visualAid += '<span class="disabling">-' + disabling.join('</span>, <span class="disabling">') + '</span>';
     }
+  }
 
-    $('span.visual-aid', $(this.element)).empty().append(visualAid);
+  if (this.visualAid == undefined) {
+    $('a span.summary', this.element).append('<span class="visual-aid"></span>');
+    this.visualAid = $('span.visual-aid', this.element);
   }
 
-  Drupal.ModuleFilter.updateVisualAid = function(type, row) {
-    // Find row class.
-    var classes = row.attr('class').split(' ');
+  this.visualAid.empty().append(visualAid);
+};
+
+Drupal.ModuleFilter.getTabID = function($row) {
+  var id = $row.data('moduleFilterTabID');
+  if (!id) {
+    // Find the tab ID.
+    var classes = $row.attr('class').split(' ');
     for (var i in classes) {
-      // Remove '-content' so we can use as id.
-      var id = classes[i].substring(0, classes[i].length - 8);
-      if (Drupal.ModuleFilter.tabs[id] != undefined) {
+      if (Drupal.ModuleFilter.tabs[classes[i]] != undefined) {
+        id = classes[i];
         break;
       }
     }
+    $row.data('moduleFilterTabID', id);
+  }
+  return id;
+};
 
-    if (Drupal.ModuleFilter.activeTab.id == 'all-tab') {
-      var allTab = Drupal.ModuleFilter.activeTab;
-      var projectTab = Drupal.ModuleFilter.tabs[id];
-    }
-    else {
-      var allTab = Drupal.ModuleFilter.tabs['all-tab'];
-      var projectTab = Drupal.ModuleFilter.activeTab;
-    }
+Drupal.ModuleFilter.updateVisualAid = function(type, $row) {
+  var id = Drupal.ModuleFilter.getTabID($row);
 
-    var name = $('td label strong', row).text();
-    switch (type) {
-      case 'enable':
-        if (Drupal.ModuleFilter.disabling[id + name] != undefined) {
-          delete(Drupal.ModuleFilter.disabling[id + name]);
-          allTab.updateDisabling(-1);
-          projectTab.updateDisabling(-1);
-          row.removeClass('disabling');
-        }
-        else {
-          Drupal.ModuleFilter.enabling[id + name] = name;
-          allTab.updateEnabling(1);
-          projectTab.updateEnabling(1);
-          row.addClass('enabling');
-        }
-        break;
-      case 'disable':
-        if (Drupal.ModuleFilter.enabling[id + name] != undefined) {
-          delete(Drupal.ModuleFilter.enabling[id + name]);
-          allTab.updateEnabling(-1);
-          projectTab.updateEnabling(-1);
-          row.removeClass('enabling');
-        }
-        else {
-          Drupal.ModuleFilter.disabling[id + name] = name;
-          allTab.updateDisabling(1);
-          projectTab.updateDisabling(1);
-          row.addClass('disabling');
-        }
-        break;
-    }
+  if (!id) {
+    return false;
+  }
 
-    allTab.updateVisualAid();
-    projectTab.updateVisualAid();
+  var tab = Drupal.ModuleFilter.tabs[id];
+  var name = Drupal.checkPlain($('td:nth(1) strong', $row).text());
+  switch (type) {
+    case 'enable':
+      if (Drupal.ModuleFilter.disabling[id + name] != undefined) {
+        delete Drupal.ModuleFilter.disabling[id + name];
+        tab.updateDisabling(name, true);
+        $row.removeClass('disabling');
+      }
+      else {
+        Drupal.ModuleFilter.enabling[id + name] = name;
+        tab.updateEnabling(name);
+        $row.addClass('enabling');
+      }
+      break;
+    case 'disable':
+      if (Drupal.ModuleFilter.enabling[id + name] != undefined) {
+        delete Drupal.ModuleFilter.enabling[id + name];
+        tab.updateEnabling(name, true);
+        $row.removeClass('enabling');
+      }
+      else {
+        Drupal.ModuleFilter.disabling[id + name] = name;
+        tab.updateDisabling(name);
+        $row.addClass('disabling');
+      }
+      break;
   }
+
+  tab.updateVisualAid();
+};
+
+Drupal.ModuleFilter.Filter.prototype.adjustHeight = function() {
+  // Hack for adjusting the height of the modules section.
+  var minHeight = $('#module-filter-tabs ul').height() + 10;
+  minHeight += $('#module-filter-tabs #module-filter-submit').height();
+  $('#module-filter-modules').css('min-height', minHeight);
+  this.element.trigger('moduleFilter:adjustHeight');
+}
+
 })(jQuery);

+ 181 - 0
sites/all/modules/contrib/admin/module_filter/js/modules.js

@@ -0,0 +1,181 @@
+(function($) {
+
+Drupal.behaviors.moduleFilter = {
+  attach: function(context) {
+    $('#system-modules td.description').once('description', function() {
+      $(this).click(function() {
+        $('.inner.expand', $(this)).toggleClass('expanded');
+      });
+      $('.inner.expand', $(this)).children().click(function(e) {
+        if ($(this).parent().hasClass('expanded')) {
+          e.stopPropagation();
+        }
+      });
+    });
+
+    $('.module-filter-inputs-wrapper', context).once('module-filter', function() {
+      var filterInput = $('input[name="module_filter[name]"]', context);
+      var selector = '#system-modules table tbody tr';
+      if (Drupal.settings.moduleFilter.tabs) {
+        selector += '.module';
+      }
+
+      filterInput.moduleFilter(selector, {
+        wrapper: $('#module-filter-modules'),
+        delay: 500,
+        striping: true,
+        childSelector: 'td:nth(1)',
+        rules: [
+          function(moduleFilter, item) {
+            if (!item.unavailable) {
+              if (moduleFilter.options.showEnabled) {
+                if (item.status && !item.disabled) {
+                  return true;
+                }
+              }
+              if (moduleFilter.options.showDisabled) {
+                if (!item.status && !item.disabled) {
+                  return true;
+                }
+              }
+              if (moduleFilter.options.showRequired) {
+                if (item.status && item.disabled) {
+                  return true;
+                }
+              }
+            }
+            if (moduleFilter.options.showUnavailable) {
+              if (item.unavailable || (!item.status && item.disabled)) {
+                return true;
+              }
+            }
+            return false;
+          }
+        ],
+        buildIndex: [
+          function(moduleFilter, item) {
+            var $checkbox = $('td.checkbox :checkbox', item.element);
+            if ($checkbox.size() > 0) {
+              item.status = $checkbox.is(':checked');
+              item.disabled = $checkbox.is(':disabled');
+            }
+            else {
+              item.status = false;
+              item.disabled = true;
+              item.unavailable = true;
+            }
+            return item;
+          }
+        ],
+        showEnabled: $('#edit-module-filter-show-enabled').is(':checked'),
+        showDisabled: $('#edit-module-filter-show-disabled').is(':checked'),
+        showRequired: $('#edit-module-filter-show-required').is(':checked'),
+        showUnavailable: $('#edit-module-filter-show-unavailable').is(':checked')
+      });
+
+      var moduleFilter = filterInput.data('moduleFilter');
+
+      moduleFilter.operators = {
+        description: function(string, moduleFilter, item) {
+          if (item.description == undefined) {
+            var description = $('.description', item.element).clone();
+            $('.admin-requirements', description).remove();
+            $('.admin-operations', description).remove();
+            item.description = description.text().toLowerCase();
+          }
+
+          if (item.description.indexOf(string) >= 0) {
+            return true;
+          }
+        },
+        requiredBy: function(string, moduleFilter, item) {
+          if (item.requiredBy == undefined) {
+            var requirements = Drupal.ModuleFilter.getRequirements(item.element);
+            item.requires = requirements.requires;
+            item.requiredBy = requirements.requiredBy;
+          }
+
+          for (var i in item.requiredBy) {
+            if (item.requiredBy[i].indexOf(string) >= 0) {
+              return true;
+            }
+          }
+        },
+        requires: function(string, moduleFilter, item) {
+          if (item.requires == undefined) {
+            var requirements = Drupal.ModuleFilter.getRequirements(item.element);
+            item.requires = requirements.requires;
+            item.requiredBy = requirements.requiredBy;
+          }
+
+          for (var i in item.requires) {
+            if (item.requires[i].indexOf(string) >= 0) {
+              return true;
+            }
+          }
+        }
+      };
+
+      $('#edit-module-filter-show-enabled', context).change(function() {
+        moduleFilter.options.showEnabled = $(this).is(':checked');
+        moduleFilter.applyFilter();
+      });
+      $('#edit-module-filter-show-disabled', context).change(function() {
+        moduleFilter.options.showDisabled = $(this).is(':checked');
+        moduleFilter.applyFilter();
+      });
+      $('#edit-module-filter-show-required', context).change(function() {
+        moduleFilter.options.showRequired = $(this).is(':checked');
+        moduleFilter.applyFilter();
+      });
+      $('#edit-module-filter-show-unavailable', context).change(function() {
+        moduleFilter.options.showUnavailable = $(this).is(':checked');
+        moduleFilter.applyFilter();
+      });
+
+      if (!Drupal.settings.moduleFilter.tabs) {
+        moduleFilter.element.bind('moduleFilter:start', function() {
+          $('#system-modules fieldset').show();
+        });
+
+        moduleFilter.element.bind('moduleFilter:finish', function(e, data) {
+          $('#system-modules fieldset').each(function(i) {
+            $fieldset = $(this);
+            if ($('tbody tr', $fieldset).filter(':visible').length == 0) {
+              $fieldset.hide();
+            }
+          });
+        });
+
+        moduleFilter.applyFilter();
+      }
+    });
+  }
+};
+
+Drupal.ModuleFilter.getRequirements = function(element) {
+  var requires = new Array();
+  var requiredBy = new Array();
+  $('.admin-requirements', element).each(function() {
+    var text = $(this).text();
+    if (text.substr(0, 9) == 'Requires:') {
+      // Requires element.
+      requiresString = text.substr(9);
+      requires = requiresString.replace(/\([a-z]*\)/g, '').split(',');
+    }
+    else if (text.substr(0, 12) == 'Required by:') {
+      // Required by element.
+      requiredByString = text.substr(12);
+      requiredBy = requiredByString.replace(/\([a-z]*\)/g, '').split(',');
+    }
+  });
+  for (var i in requires) {
+    requires[i] = $.trim(requires[i].toLowerCase());
+  }
+  for (var i in requiredBy) {
+    requiredBy[i] = $.trim(requiredBy[i].toLowerCase());
+  }
+  return { requires: requires, requiredBy: requiredBy };
+};
+
+})(jQuery);

+ 67 - 0
sites/all/modules/contrib/admin/module_filter/js/permissions.js

@@ -0,0 +1,67 @@
+(function($) {
+
+var lastModuleItem;
+
+Drupal.behaviors.moduleFilterPermissions = {
+  attach: function(context) {
+    $('.module-filter-inputs-wrapper', context).once('module-filter', function() {
+      var filterInput = $('input[name="module_filter[name]"]', context);
+      var selector = '#permissions tbody tr';
+
+      // Move location of filter input.
+      $('#permissions').parent().prepend(filterInput.parent().parent());
+
+      filterInput.moduleFilter(selector, {
+        wrapper: $('#permissions').parent(),
+        childSelector: 'td.module',
+        buildIndex: [
+          function(moduleFilter, item) {
+            item.isModule = (item.text != '') ? true : false;
+            if (item.isModule) {
+              item.children = new Array();
+              lastModuleItem = item;
+            }
+            else {
+              item.parent = lastModuleItem;
+              lastModuleItem.children.push(item);
+            }
+            return item;
+          }
+        ]
+      });
+
+      var moduleFilter = filterInput.data('moduleFilter');
+
+      moduleFilter.operators = {
+        perm: function(string, moduleFilter, item) {
+          if (!item.isModule) {
+            if (item.name == undefined) {
+              var $name = $('td.permission', item.element).clone();
+              $('.description', $name).remove();
+              item.name = $name.text().trim().toLowerCase();
+            }
+
+            if (item.name.indexOf(string) >= 0) {
+              return true;
+            }
+          }
+        }
+      };
+
+      moduleFilter.element.bind('moduleFilter:finish', function(e, data) {
+        for (var i in moduleFilter.results) {
+          if (moduleFilter.results[i].isModule) {
+            for (var k in moduleFilter.results[i].children) {
+              moduleFilter.results[i].children[k].element.removeClass('js-hide');
+            }
+          }
+          else {
+            moduleFilter.results[i].parent.element.removeClass('js-hide');
+          }
+        }
+      });
+    });
+  }
+};
+
+})(jQuery);

+ 117 - 0
sites/all/modules/contrib/admin/module_filter/js/update_status.js

@@ -0,0 +1,117 @@
+(function($) {
+
+Drupal.behaviors.moduleFilterUpdateStatus = {
+  attach: function(context) {
+    $('#module-filter-update-status-form').once('update-status', function() {
+      var filterInput = $('input[name="module_filter[name]"]', context);
+      filterInput.moduleFilter('table.update > tbody > tr', {
+        wrapper: $('table.update:first').parent(),
+        delay: 300,
+        childSelector: 'div.project a',
+        rules: [
+          function(moduleFilter, item) {
+            switch (moduleFilter.options.show) {
+              case 'all':
+                return true;
+              case 'updates':
+                if (item.state == 'warning' || item.state == 'error') {
+                  return true;
+                }
+                break;
+              case 'security':
+                if (item.state == 'error') {
+                  return true;
+                }
+                break;
+              case 'ignore':
+                if (item.state == 'ignored') {
+                  return true;
+                }
+                break;
+              case 'unknown':
+                if (item.state == 'unknown') {
+                  return true;
+                }
+                break;
+            }
+            return false;
+          }
+        ],
+        buildIndex: [
+          function(moduleFilter, item) {
+            if ($('.version-status', item.element).text() == Drupal.t('Ignored from settings')) {
+              item.state = 'ignored';
+              return item;
+            }
+            if (item.element.is('.ok')) {
+              item.state = 'ok';
+            }
+            else if (item.element.is('.warning')) {
+              item.state = 'warning';
+            }
+            else if (item.element.is('.error')) {
+              item.state = 'error';
+            }
+            else if (item.element.is('.unknown')) {
+              item.state = 'unknown';
+            }
+            return item;
+          }
+        ],
+        show: $('#edit-module-filter-show input[name="module_filter[show]"]', context).val()
+      });
+
+      var moduleFilter = filterInput.data('moduleFilter');
+
+      if (Drupal.settings.moduleFilter.rememberUpdateState) {
+        var updateShow = Drupal.ModuleFilter.getState('updateShow');
+        if (updateShow) {
+          moduleFilter.options.show = updateShow;
+          $('#edit-module-filter-show input[name="module_filter[show]"][value="' + updateShow + '"]', context).click();
+        }
+      }
+
+      $('#edit-module-filter-show input[name="module_filter[show]"]', context).change(function() {
+        moduleFilter.options.show = $(this).val();
+        Drupal.ModuleFilter.setState('updateShow', moduleFilter.options.show);
+        moduleFilter.applyFilter();
+      });
+
+      moduleFilter.element.bind('moduleFilter:start', function() {
+        $('table.update').each(function() {
+          $(this).show().prev('h3').show();
+        });
+      });
+
+      moduleFilter.element.bind('moduleFilter:finish', function(e, data) {
+        $('table.update').each(function() {
+          var $table = $(this);
+          if ($('tbody tr', $(this)).filter(':visible').length == 0) {
+            $table.hide().prev('h3').hide();
+          }
+        });
+      });
+
+      moduleFilter.element.bind('moduleFilter:keyup', function() {
+        if (moduleFilter.clearOffset == undefined) {
+          moduleFilter.inputWidth = filterInput.width();
+          moduleFilter.clearOffset = moduleFilter.element.parent().find('.module-filter-clear a').width();
+        }
+        if (moduleFilter.text) {
+          filterInput.width(moduleFilter.inputWidth - moduleFilter.clearOffset - 5).parent().css('margin-right', moduleFilter.clearOffset + 5);
+        }
+        else {
+          filterInput.width(moduleFilter.inputWidth).parent().css('margin-right', 0);
+        }
+      });
+
+      moduleFilter.element.parent().find('.module-filter-clear a').click(function() {
+        filterInput.width(moduleFilter.inputWidth).parent().css('margin-right', 0);
+      });
+
+      moduleFilter.applyFilter();
+    });
+  }
+};
+
+})(jQuery);

+ 78 - 11
sites/all/modules/contrib/admin/module_filter/module_filter.admin.inc

@@ -14,36 +14,103 @@
  * Settings form for module filter.
  */
 function module_filter_settings() {
+  $form['module_filter_set_focus'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Set focus to filter field on page load'),
+    '#description' => t('Currently has no effect when using Overlay module.'),
+    '#default_value' => variable_get('module_filter_set_focus', 1),
+  );
+
   $form['module_filter_tabs'] = array(
     '#type' => 'checkbox',
-    '#title' => t('Tabs'),
-    '#description' => t('Divide module groups into tabbed list.'),
-    '#default_value' => variable_get('module_filter_tabs', 1)
+    '#title' => t('Enhance the modules page with tabs'),
+    '#description' => t('Alternate tabbed theme that restructures packages into tabs.'),
+    '#default_value' => variable_get('module_filter_tabs', 1),
   );
   $form['tabs'] = array(
     '#type' => 'fieldset',
     '#title' => t('Tabs'),
     '#description' => t('Settings used with the tabs view of the modules page.'),
     '#collapsible' => TRUE,
-    '#collapsed' => FALSE
+    '#collapsed' => FALSE,
   );
   $form['tabs']['module_filter_count_enabled'] = array(
     '#type' => 'checkbox',
     '#title' => t('Number of enabled modules'),
     '#description' => t('Display the number of enabled modules in the active tab along with the total number of modules.'),
-    '#default_value' => variable_get('module_filter_count_enabled', 1)
+    '#default_value' => variable_get('module_filter_count_enabled', 1),
   );
   $form['tabs']['module_filter_visual_aid'] = array(
     '#type' => 'checkbox',
-    '#title' => t('Visuals for newly enabled and disabled modules'),
-    '#description' => t("Adds a basic count to tabs of modules being enabled/disabled and colors the module row pending it's being enabled or disabled"),
-    '#default_value' => variable_get('module_filter_visual_aid', 1)
+    '#title' => t('Visual aids'),
+    '#description' => t('When enabling/disabling modules, the module name will display in the tab summary.<br />When filtering, a count of results for each tab will be presented.'),
+    '#default_value' => variable_get('module_filter_visual_aid', 1),
+  );
+  $form['tabs']['module_filter_hide_empty_tabs'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Hide tabs with no results'),
+    '#description' => t('When a filter returns no results for a tab, the tab is hidden. This is dependent on visual aids being enabled.'),
+    '#default_value' => variable_get('module_filter_hide_empty_tabs', 0),
   );
   $form['tabs']['module_filter_dynamic_save_position'] = array(
     '#type' => 'checkbox',
-    '#title' => t('Save dynamic positioning'),
-    '#description' => t("DEVELOPMENTAL: For sites with lots of tabs, enable to help keep the 'Save configuration' button more accessible."),
-    '#default_value' => variable_get('module_filter_dynamic_save_position', 0)
+    '#title' => t('Dynamically position Save button'),
+    '#description' => t("For sites with lots of tabs, enable to help keep the 'Save configuration' button more accessible."),
+    '#default_value' => variable_get('module_filter_dynamic_save_position', 1),
+  );
+  $form['tabs']['module_filter_use_url_fragment'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Use URL fragment'),
+    '#description' => t('Use URL fragment when navigating between tabs. This lets you use the browsers back/forward buttons to navigate through the tabs you selected.') . '<br />' . t('When the Overlay module is enabled this functionality will not be used since overlay relies on the URL fragment.'),
+    '#default_value' => variable_get('module_filter_use_url_fragment', 1),
+  );
+  $form['tabs']['module_filter_use_switch'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Use switch instead of checkbox'),
+    '#description' => t('This is purely cosmetic (at least for now). Displays a ON/OFF switch rather than a checkbox to enable/disable modules.<br /><strong>Modules will not actually be enabled/disabled until the form is saved.</strong>'),
+    '#default_value' => variable_get('module_filter_use_switch', 1),
+  );
+  $form['tabs']['module_filter_track_recent_modules'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Track recently enabled/disabled modules'),
+    '#description' => t('Adds a "Recent" tab that displays modules that have been enabled or disabled with the last week.'),
+    '#default_value' => variable_get('module_filter_track_recent_modules', 1),
+  );
+  $form['tabs']['module_filter_remember_active_tab'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Remember active tab.'),
+    '#description' => t('When enabled, the active tab will be remembered.'),
+    '#default_value' => variable_get('module_filter_remember_active_tab', 1),
+  );
+  $form['tabs']['module_filter_version_column'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Place version in own column'),
+    '#description' => t("Moves the version out of the description and into it's own column"),
+    '#default_value' => variable_get('module_filter_version_column', 0),
   );
+  $form['tabs']['module_filter_expanded_description'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Expand description by default'),
+    '#description' => t('When enabled, the description will be expanded by default.'),
+    '#default_value' => variable_get('module_filter_expanded_description', 0),
+  );
+
+  $form['update'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Update status'),
+    '#collapsible' => TRUE,
+    '#collapsed' => (module_exists('update')) ? FALSE : TRUE,
+  );
+  $form['update']['module_filter_remember_update_state'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Remember the last selected filter.'),
+    '#description' => t('When enabled, the last state (All, Update available, Security update, Unknown) will be remembered.'),
+    '#default_value' => variable_get('module_filter_remember_update_state', 0),
+  );
+
+  if (module_exists('page_actions')) {
+    $form['tabs']['module_filter_dynamic_save_position']['#description'] .= '<br />' . t('The module %name is enabled and thus this setting will have no affect.', array('%name' => t('Page actions')));
+  }
+
   return system_settings_form($form);
 }

+ 4 - 5
sites/all/modules/contrib/admin/module_filter/module_filter.info

@@ -1,9 +1,9 @@
 name = Module filter
 description = "Filter the modules list."
 core = 7.x
+package = Administration
 
 files[] = module_filter.install
-files[] = module_filter.js
 files[] = module_filter.module
 files[] = module_filter.admin.inc
 files[] = module_filter.theme.inc
@@ -16,9 +16,8 @@ files[] = js/module_filter_tab.js
 
 configure = admin/config/user-interface/modulefilter
 
-; Information added by drupal.org packaging script on 2012-07-05
-version = "7.x-1.7"
+; Information added by Drupal.org packaging script on 2019-03-27
+version = "7.x-2.2"
 core = "7.x"
 project = "module_filter"
-datestamp = "1341518501"
-
+datestamp = "1553698385"

+ 28 - 1
sites/all/modules/contrib/admin/module_filter/module_filter.install

@@ -5,13 +5,22 @@
  */
 
 /**
- * Implementation of hook_uninstall().
+ * Implements hook_uninstall().
  */
 function module_filter_uninstall() {
+  variable_del('module_filter_set_focus');
   variable_del('module_filter_tabs');
   variable_del('module_filter_count_enabled');
   variable_del('module_filter_visual_aid');
+  variable_del('module_filter_hide_empty_tabs');
   variable_del('module_filter_dynamic_save_position');
+  variable_del('module_filter_use_url_fragment');
+  variable_del('module_filter_use_switch');
+  variable_del('module_filter_track_recent_modules');
+  variable_del('module_filter_remember_active_tab');
+  variable_del('module_filter_remember_update_state');
+  variable_del('module_filter_version_column');
+  variable_del('module_filter_expanded_description');
 }
 
 /**
@@ -20,3 +29,21 @@ function module_filter_uninstall() {
 function module_filter_update_7100() {
   variable_del('module_filter_autocomplete');
 }
+
+/**
+ * Rebuild the menu and theme registry.
+ */
+function module_filter_update_7200() {
+  menu_rebuild();
+  system_rebuild_theme_data();
+  drupal_theme_rebuild();
+}
+
+/**
+ * Old update that use to remove the module_filter_dynamic_save_position variable.
+ */
+function module_filter_update_7201() {
+  // We don't want to remove this update hook but at the same time we no
+  // longer want to lose the variable setting, so we just comment it out.
+  // variable_del('module_filter_dynamic_save_position');
+}

Some files were not shown because too many files changed in this diff