diff --git a/modules/core/functions.php b/modules/core/functions.php
index 9ade1c45e6..39b06fd0eb 100644
--- a/modules/core/functions.php
+++ b/modules/core/functions.php
@@ -443,6 +443,7 @@ function setup_base_page($name, $source=false, $use_layout=true) {
add_output($name, 'folder_list_end', true, $source);
add_output($name, 'content_section_start', true, $source);
add_output($name, 'content_section_end', true, $source);
+ add_output($name, 'modals', true, $source);
add_output($name, 'save_reminder', true, $source);
add_output($name, 'content_end', false, $source, 'page_js', 'after');
}
diff --git a/modules/core/output_modules.php b/modules/core/output_modules.php
index c745a67b3d..135b3fb2a5 100644
--- a/modules/core/output_modules.php
+++ b/modules/core/output_modules.php
@@ -1647,6 +1647,112 @@ protected function output() {
}
}
+/**
+ * modals
+ * @subpackage core/output
+ */
+class Hm_Output_modals extends Hm_Output_Module {
+ /**
+ * Outputs modals
+ */
+ protected function output() {
+ $share_folder_modal = '
';
+ $share_folder_modal .= '
';
+ $share_folder_modal .= '
';
+ $share_folder_modal .= '';
+
+ $share_folder_modal .= '
';
+ $share_folder_modal .= '
';
+
+ $share_folder_modal .= '
';
+
+ $share_folder_modal .= '
';
+ $share_folder_modal .= '
';
+ $share_folder_modal .= 'Loading...';
+ $share_folder_modal .= '
';
+ $share_folder_modal .= '
';
+
+ $share_folder_modal .= '
';
+ $share_folder_modal .= '';
+ $share_folder_modal .= '';
+ $share_folder_modal .= ''.$this->trans('User').' | ';
+ $share_folder_modal .= ''.$this->trans('Permissions').' | ';
+ $share_folder_modal .= ''.$this->trans('Actions').' | ';
+ $share_folder_modal .= '
';
+ $share_folder_modal .= '';
+ $share_folder_modal .= '';
+ $share_folder_modal .= '
';
+
+ $share_folder_modal .= '
';
+
+ $share_folder_modal .= '
';
+ $share_folder_modal .= '
';
+ $share_folder_modal .= '
';
+
+ $share_folder_modal .= '
';
+ $share_folder_modal .= '
';
+
+ $share_folder_modal .= '
';
+ $share_folder_modal .= '
';
+ $share_folder_modal .= '
';
+
+ return $share_folder_modal;
+ }
+}
+
/**
* Starts the message view page
* @subpackage core/output
diff --git a/modules/core/site.js b/modules/core/site.js
index c22f5700f4..e9bf2e852f 100644
--- a/modules/core/site.js
+++ b/modules/core/site.js
@@ -58,6 +58,18 @@ $.fn.fadeOutAndRemove = function(timeout = 600) {
return this;
};
+$.fn.modal = function(action) {
+ const modalElement = this[0];
+ if (modalElement) {
+ const modal = new bootstrap.Modal(modalElement);
+ if (action === 'show') {
+ modal.show();
+ } else if (action === 'hide') {
+ modal.hide();
+ }
+ }
+};
+
/* swipe event handler */
var swipe_event = function(el, callback, direction) {
var start_x, start_y, dist_x, dist_y, threshold = 150, restraint = 100,
diff --git a/modules/imap/functions.php b/modules/imap/functions.php
index 02ee8b9053..ca57d4846d 100644
--- a/modules/imap/functions.php
+++ b/modules/imap/functions.php
@@ -111,7 +111,7 @@ function prepare_imap_message_list($msgs, $mod, $type) {
* @return string
*/
if (!hm_exists('format_imap_folder_section')) {
-function format_imap_folder_section($folders, $id, $output_mod, $with_input = false) {
+function format_imap_folder_section($folders, $id, $output_mod, $with_input = false, $can_share_folders = false) {
$results = '';
$manage = $output_mod->get('imap_folder_manage_link');
@@ -131,7 +131,7 @@ function format_imap_folder_section($folders, $id, $output_mod, $with_input = fa
$attrs .= ' class="folder-disabled"';
}
} else {
- $attrs = 'data-id="imap_'.$id.'_'.$output_mod->html_safe($folder_name).
+ $attrs = 'id="main-link" data-id="imap_'.$id.'_'.$output_mod->html_safe($folder_name).
'" href="?page=message_list&list_path='.
urlencode('imap_'.$id.'_'.$output_mod->html_safe($folder_name)).'"';
}
@@ -150,7 +150,11 @@ function format_imap_folder_section($folders, $id, $output_mod, $with_input = fa
if ($with_input) {
$results .= '';
}
- $results .= '';
+ $results .= '';
+ if($can_share_folders) {
+ $results .= '';
+ }
+ $results .= '';
}
if ($manage) {
$results .= '- '.$output_mod->trans('Manage Folders').'
';
diff --git a/modules/imap/handler_modules.php b/modules/imap/handler_modules.php
index ca5b0f0489..7868b485cd 100644
--- a/modules/imap/handler_modules.php
+++ b/modules/imap/handler_modules.php
@@ -755,6 +755,7 @@ public function process() {
$with_subscription = isset($this->request->post['subscription_state']) && $this->request->post['subscription_state'];
$cache = Hm_IMAP_List::get_cache($this->cache, $form['imap_server_id']);
$imap = Hm_IMAP_List::connect($form['imap_server_id'], $cache);
+ $this->out('can_share_folders', stripos($imap->get_capability(), 'ACL') !== false);
if (imap_authed($imap)) {
$quota_root = $imap->get_quota_root($folder ? $folder : 'INBOX');
if ($quota_root && isset($quota_root[0]['name'])) {
@@ -1630,6 +1631,59 @@ public function process() {
}
}
}
+/**
+ * Load IMAP servers permissions for shared folders
+ * @subpackage imap/handler
+ */
+class Hm_Handler_load_imap_folders_permissions extends Hm_Handler_Module {
+ /**
+ * Output IMAP server permissions array for shared folders
+ */
+ public function process() {
+ list($success, $form) = $this->process_form(array('imap_server_id','imap_folder_uid','imap_folder'));
+
+ if ($success && !empty($form['imap_server_id']) && !empty($form['imap_folder']) && !empty($form['imap_folder_uid'])) {
+ Hm_IMAP_List::init($this->user_config, $this->session);
+ $server = Hm_IMAP_List::dump($form['imap_server_id'], true);
+ $cache = Hm_IMAP_List::get_cache($this->cache, $form['imap_server_id']);
+
+ $imap = Hm_IMAP_List::connect($form['imap_server_id'], $cache, $server['user'], $server['pass']);
+ $permissions = $imap->get_acl($form['imap_folder']);
+ $this->out('imap_folders_permissions', $permissions);
+ }
+ }
+}
+
+/**
+ * Load IMAP servers permissions for shared folders
+ * @subpackage imap/handler
+ */
+class Hm_Handler_set_acl_to_imap_folders extends Hm_Handler_Module {
+ /**
+ * Output IMAP server permissions array for shared folders
+ */
+ public function process() {
+ list($success, $form) = $this->process_form(array('imap_server_id','imap_folder','identifier','permissions','action'));
+
+ if ($success && !empty($form['imap_server_id']) && !empty($form['identifier']) && !empty($form['permissions']) && !empty($form['action'])) {
+
+ Hm_IMAP_List::init($this->user_config, $this->session);
+ $server = Hm_IMAP_List::dump($form['imap_server_id'], true);
+ $cache = Hm_IMAP_List::get_cache($this->cache, $form['imap_server_id']);
+
+ $imap = Hm_IMAP_List::connect($form['imap_server_id'], $cache, $server['user'], $server['pass']);
+ if($form['action'] === 'add') {
+ $response = $imap->set_acl($form['imap_folder'], $form['identifier'], $form['permissions']);
+ } else {
+ $response = $imap->delete_acl($form['imap_folder'], $form['identifier']);
+ }
+ if($response) {
+ $permissions = $imap->get_acl($form['imap_folder']);
+ $this->out('imap_folders_permissions', $permissions);
+ }
+ }
+ }
+}
/**
* Load IMAP servers for the user config object
diff --git a/modules/imap/hm-imap.php b/modules/imap/hm-imap.php
index 8d002fc808..a15444893a 100644
--- a/modules/imap/hm-imap.php
+++ b/modules/imap/hm-imap.php
@@ -369,6 +369,120 @@ public function get_capability() {
return $this->capability;
}
+ /**
+ * Sets the Access Control List (ACL) for a specified mailbox.
+ *
+ * This function sends the `SETACL` command to the IMAP server to modify
+ * the access rights of a user or identifier for a given mailbox. The access rights
+ * can either be granted or revoked based on the rights modification string.
+ *
+ * The third argument can either:
+ * - Add rights (using a `+` prefix),
+ * - Remove rights (using a `-` prefix),
+ * - Completely replace existing rights (no prefix).
+ *
+ * For more information on ACLs, see RFC 4314:
+ * [Access Control Lists (ACLs) in Internet Message Access Protocol (IMAP)](https://tools.ietf.org/html/rfc4314).
+ *
+ * @param string $mailbox_name The name of the mailbox for which the ACL is to be set.
+ * @param string $identifier The user or identifier whose permissions are being modified.
+ * @param string $rights_modification The modification to the user's rights (e.g., "+rw" to add read and write access,
+ * "-w" to remove write access, or "rw" to set read and write access explicitly).
+ *
+ * @return bool True if the ACL was successfully set, false if an error occurred.
+ */
+ public function set_acl($mailbox_name, $identifier, $rights_modification) {
+ $command = "SETACL \"$mailbox_name\" \"$identifier\" \"$rights_modification\"\r\n";
+
+ $this->send_command($command);
+ $response = $this->get_response();
+
+ foreach ($response as $line) {
+ if (mb_strpos($line, 'OK') !== false) {
+ Hm_Msgs::add('Permissions added successfully');
+ return true;
+ }
+ elseif (mb_strpos($line, 'NO') !== false || mb_strpos($line, 'BAD') !== false) {
+ $this->debug[] = 'SETACL failed: ' . $line;
+ Hm_Msgs::add('ERRSETACL failed:' . $line);
+ return false;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Deletes an access control list (ACL) entry for a specified identifier on a given mailbox.
+ *
+ * This function sends a DELETEACL command to remove any pair
+ * for the specified identifier from the access control list for the specified mailbox.
+ *
+ * For more information on ACLs, see RFC 4314:
+ * [Access Control Lists (ACLs) in Internet Message Access Protocol (IMAP)](https://tools.ietf.org/html/rfc4314).
+ *
+ * @param string $mailbox The name of the mailbox from which to delete the ACL entry.
+ * @param string $identifier The authentication identifier whose rights are to be removed.
+ *
+ * @throws Exception If the delete operation fails or if invalid arguments are provided.
+ *
+ * @return bool Returns true on successful deletion of the ACL entry; otherwise, false.
+ */
+ public function delete_acl($mailbox_name, $identifier) {
+ $command = "DELETEACL \"$mailbox_name\" \"$identifier\"\r\n";
+ $this->send_command($command);
+ $response = $this->get_response();
+
+ foreach ($response as $line) {
+ if (strpos($line, 'OK') !== false) {
+ Hm_Msgs::add('Permissions removed successfully');
+ return true;
+ } else {
+ $this->debug[] = 'DELETEACL failed: ' . $line;
+ Hm_Msgs::add('ERRDELETEACL failed: ailure: can\'t delete acl');
+ return false;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Retrieves the Access Control List (ACL) for a specified mailbox.
+ *
+ * This function sends a `GETACL` command to the IMAP server to fetch the list
+ * of users and their respective access rights for the given mailbox. It then
+ * parses the server's response and maps the raw rights to human-readable
+ * permissions using the `map_permissions` function.
+ *
+ * For more information on ACLs, see RFC 4314:
+ * [Access Control Lists (ACLs) in Internet Message Access Protocol (IMAP)](https://tools.ietf.org/html/rfc4314).
+ *
+ * @param string $mailbox_name The name of the mailbox for which to retrieve the ACL.
+ *
+ * @return array An associative array where the keys are email addresses (or user identifiers),
+ * and the values are human-readable permissions (e.g., 'Read, Write').
+ */
+ public function get_acl($mailbox_name) {
+ $acl_list = [];
+ $command = "GETACL \"$mailbox_name\"\r\n";
+ $this->send_command($command);
+ $response = $this->get_response();
+
+ foreach ($response as $line) {
+ if (preg_match('/^\* ACL ([^\s]+) (.+)$/', $line, $matches)) {
+ $mailbox = $matches[1];
+ $acl_string = $matches[2];
+
+ $acl_parts = explode(' ', $acl_string);
+ for ($i = 1; $i < count($acl_parts); $i += 2) {
+ $user = $acl_parts[$i - 1];
+ $rights = $acl_parts[$i];
+ $acl_list[$user] = $this->map_permissions($rights);
+ }
+ }
+ }
+ return $acl_list;
+ }
+
/**
* special version of LIST to return just special use mailboxes
* @param string $type type of special folder to return (sent, all, trash, flagged, junk)
@@ -892,6 +1006,43 @@ public function get_message_structure($uid) {
return $struct;
}
+ /**
+ * This function maps raw IMAP rights to human-readable permissions.
+ *
+ * It takes the raw IMAP rights string, such as 'lrws' and converts it into a more
+ * understandable format like 'Lookup, Read, Write, Write Seen'.
+ *
+ * @param string $rights The string of IMAP rights (e.g., 'lrws'), where each character
+ * represents a specific permission.
+ *
+ * @return string A comma-separated string of human-readable permissions.
+ */
+ private function map_permissions($rights_string) {
+ $permissions = [];
+ $permission_map = [
+ 'l' => 'Lookup',
+ 'r' => 'Read',
+ 's' => 'See',
+ 'w' => 'Write',
+ 'i' => 'Insert',
+ 'p' => 'Post',
+ 'k' => 'Administer',
+ 'x' => 'Delete Mailbox',
+ 't' => 'Take',
+ 'e' => 'Examine',
+ 'c' => 'Create',
+ 'd' => 'Delete'
+ ];
+
+ foreach (str_split($rights_string) as $char) {
+ if (isset($permission_map[$char])) {
+ $permissions[] = $permission_map[$char];
+ }
+ }
+
+ return implode(', ', $permissions);
+ }
+
/**
* get the raw IMAP BODYSTRUCTURE response
* @param int $uid IMAP UID of the message
diff --git a/modules/imap/output_modules.php b/modules/imap/output_modules.php
index 2d795e5b1e..30f764fa2f 100644
--- a/modules/imap/output_modules.php
+++ b/modules/imap/output_modules.php
@@ -778,8 +778,9 @@ protected function output() {
$folder_data = $this->get('imap_expanded_folder_data', array());
$with_input = $this->get('with_input', false);
$folder = $this->get('folder', '');
+ $can_share_folders = $this->get('can_share_folders', false);
if (!empty($folder_data)) {
- $res .= format_imap_folder_section($folder_data, $this->get('imap_expanded_folder_id'), $this, $with_input);
+ $res .= format_imap_folder_section($folder_data, $this->get('imap_expanded_folder_id'), $this, $with_input, $can_share_folders);
$quota = $this->get('quota');
$quota_max = $this->get('quota_max');
if (!$folder && $quota) {
@@ -790,6 +791,15 @@ protected function output() {
}
}
+/**
+ * @subpackage imap/output
+ */
+class Hm_Output_get_list_imap_folders_permissions extends Hm_Output_Module {
+ public function output() {
+ $this->out('ajax_imap_folders_permissions', $this->get('imap_folders_permissions', array()));
+ }
+}
+
/**
* Add move/copy dialog to the message list controls
* @subpackage imap/output
diff --git a/modules/imap/setup.php b/modules/imap/setup.php
index 71cac98234..d6413d1250 100644
--- a/modules/imap/setup.php
+++ b/modules/imap/setup.php
@@ -318,6 +318,12 @@
add_handler('ajax_imap_unsnooze', 'save_imap_cache', true);
add_handler('ajax_imap_unsnooze', 'imap_unsnooze_message', true, 'core');
+/* share folders */
+setup_base_ajax_page('ajax_share_folders', 'core');
+add_handler('ajax_share_folders', 'load_imap_folders_permissions', true);
+add_output('ajax_share_folders', 'get_list_imap_folders_permissions', true);
+add_handler('ajax_share_folders', 'set_acl_to_imap_folders', true);
+
/* allowed input */
return array(
'allowed_pages' => array(
@@ -346,6 +352,7 @@
'ajax_imap_unsnooze',
'ajax_imap_junk',
'message_source',
+ 'ajax_share_folders',
),
'allowed_output' => array(
@@ -368,6 +375,7 @@
'snoozed_messages' => array(FILTER_VALIDATE_INT, false),
'auto_advance_email_enabled' => array(FILTER_VALIDATE_BOOLEAN, false),
'do_not_flag_as_read_on_open' => array(FILTER_VALIDATE_BOOLEAN, false),
+ 'ajax_imap_folders_permissions' => array(FILTER_UNSAFE_RAW, FILTER_REQUIRE_ARRAY),
),
'allowed_get' => array(
@@ -438,5 +446,10 @@
'tag_id' => FILTER_DEFAULT,
'first_time_screen_emails' => FILTER_VALIDATE_INT,
'move_messages_in_screen_email' => FILTER_VALIDATE_BOOLEAN,
+ 'imap_folder_uid' => FILTER_DEFAULT,
+ 'imap_folder' => FILTER_DEFAULT,
+ 'identifier' => FILTER_DEFAULT,
+ 'permissions' => FILTER_DEFAULT,
+ 'action' => FILTER_DEFAULT,
)
);
diff --git a/modules/imap/site.css b/modules/imap/site.css
index 90d245c37d..7dad732212 100644
--- a/modules/imap/site.css
+++ b/modules/imap/site.css
@@ -534,4 +534,29 @@
.fade-in {
animation-name: fadeIn;
+}
+li[class^="imap_"] .inner_list li[class^="imap_"] {
+ /* display: flex;
+ align-items: center; */
+ position: relative;
+ display: flex;
+ align-items: center;
+ border-radius: 4px;
+ margin: 2px 0;
+ transition: background-color .3s;
+}
+.inner_list li[class^="imap_"] #main-link {
+ flex-grow: 1;
+}
+.inner_list li[class^="imap_"] .action-link {
+ opacity: 0;
+}
+.inner_list li[class^="imap_"]:hover .action-link {
+ opacity: 1;
+}
+#loadingSpinner {
+ position: absolute;
+ left: 33.5%;
+ top: 50%;
+ transform: translate(-50%, -50%);
}
\ No newline at end of file
diff --git a/modules/imap/site.js b/modules/imap/site.js
index 4a16d00227..be84fcb62c 100644
--- a/modules/imap/site.js
+++ b/modules/imap/site.js
@@ -1424,3 +1424,131 @@ $('.screen-email-like').on("click", function() {
});
add_email_in_contact_trusted(list_email); return false;
});
+
+$(document).on('click', '[data-bs-dismiss="modal"]', function() {
+ $('#shareFolderModal').modal('hide');
+});
+
+$(document).on('click', 'a.dropdown-item.share', function(e) {
+ e.preventDefault();
+ const listItem = e.target.closest('li');
+ if(listItem) {
+ listItem.getAttribute('data-id');
+ const uid = listItem.getAttribute('data-id');
+ const folder_uid = listItem.getAttribute('data-folder-uid');
+ const folder = listItem.getAttribute('data-folder');
+ $('#server_id').val(uid);
+ $('#folder_uid').val(folder_uid);
+ $('#folder').val(folder);
+ const currentLabel = $('#shareFolderModalLabel').text();
+ $('#shareFolderModalLabel').text(`${currentLabel} - ${folder} Folder`);
+
+ $('#shareFolderModalLabel').val(`Share ${folder} Folder`);
+ $('#shareFolderModal table tbody').empty();
+ $('#loadingSpinner').show();
+ Hm_Ajax.request(
+ [
+ { name: 'hm_ajax_hook', value: 'ajax_share_folders' },
+ { name: 'imap_server_id', value: uid },
+ { name: 'imap_folder_uid', value: folder_uid },
+ { name: 'imap_folder', value: folder },
+ ],
+ function (res) {
+ $('#loadingSpinner').hide();
+ if (res.ajax_imap_folders_permissions) {
+ const permissions = res.ajax_imap_folders_permissions;
+ //then populate the modal with the data
+ populate_permissions_table(permissions);
+ $('#permissionTable').show();
+ }
+ }
+ );
+ $('#shareFolderModal').modal('show');
+ }
+});
+
+var populate_permissions_table = function(permissions) {
+ $('#shareFolderModal table tbody').empty();
+ for (const [email, permissionList] of Object.entries(permissions)) {
+ const translatedPermissions = permissionList.split(',').map(permission => {
+ return hm_trans(permission.trim()); // Translate each permission
+ });
+ const permissionsString = translatedPermissions.join(', ');
+ const row = `
+
+ ${email} |
+ ${permissionsString} |
+
+
+ |
+
+ `;
+ $('#shareFolderModal table tbody').append(row);
+ }
+}
+
+$(document).on('click', '.edit-permission', function(e) {
+ e.preventDefault();
+
+ const email = $(this).data('email');
+ const permissions = $(this).data('permissions');
+
+ $('#email').val(email);
+ $('#identifierUser').prop('checked', true);
+
+
+ // Uncheck all permissions initially
+ $('#accessRead').prop('checked', false);
+ $('#accessWrite').prop('checked', false);
+ $('#accessDelete').prop('checked', false);
+ $('#accessOther').prop('checked', false);
+
+ // Map the permissions string to checkboxes
+ if (permissions.includes('Read')) $('#accessRead').prop('checked', true);
+ if (permissions.includes('Write')) $('#accessWrite').prop('checked', true);
+ if (permissions.includes('Delete')) $('#accessDelete').prop('checked', true);
+ if (permissions.includes('Administer') || permissions.includes('Other')) $('#accessOther').prop('checked', true);
+
+ // Show the form for editing
+ $('#shareFolderModal').modal('show');
+});
+
+$(document).on('submit', '#shareForm', function(e) {
+ e.preventDefault();
+ const server_id = $('#server_id').val();
+ const folder = $('#folder').val();
+
+ let identifier = '';
+ if ($('#identifierUser').is(':checked')) {
+ identifier = $('#email').val();
+ } else if ($('#identifierAll').is(':checked')) {
+ identifier = 'all';
+ } else if ($('#identifierGuests').is(':checked')) {
+ identifier = 'guests';
+ }
+
+ let permissions = '';
+ if ($('#accessRead').is(':checked')) permissions += 'r';
+ if ($('#accessWrite').is(':checked')) permissions += 'w';
+ if ($('#accessDelete').is(':checked')) permissions += 'd';
+ if ($('#accessOther').is(':checked')) permissions += 'a';
+ // If no permissions are selected, call DELETEACL elser call SETACL
+ const action = permissions === '' ? 'remove' : 'add';
+ Hm_Ajax.request(
+ [
+ { name: 'hm_ajax_hook', value: 'ajax_share_folders' },
+ { name: 'imap_server_id', value: server_id },
+ { name: 'identifier', value: identifier },
+ { name: 'imap_folder', value: folder },
+ { name: 'action', value: action },
+ { name: 'permissions', value: permissions },
+ ],
+ function (res) {
+ if(res.ajax_imap_folders_permissions) {
+ console.log("ajax_imap_folders_permissions",res.ajax_imap_folders_permissions);
+ const permissions = res.ajax_imap_folders_permissions;
+ populate_permissions_table(permissions);
+ }
+ }
+ );
+});
diff --git a/tests/phpunit/modules/core/functions.php b/tests/phpunit/modules/core/functions.php
index d0f7b383b7..818cf4018e 100644
--- a/tests/phpunit/modules/core/functions.php
+++ b/tests/phpunit/modules/core/functions.php
@@ -171,7 +171,7 @@ public function test_setup_base_page() {
$res2 = Hm_Output_Modules::dump();
$len2 = count($res2['foo']);
$this->assertEquals(12, $len);
- $this->assertEquals(19, $len2);
+ $this->assertEquals(20, $len2);
}
/**
* @preserveGlobalState disabled
diff --git a/tests/phpunit/modules/core/output_modules.php b/tests/phpunit/modules/core/output_modules.php
index a6853ca96c..41b71454e9 100644
--- a/tests/phpunit/modules/core/output_modules.php
+++ b/tests/phpunit/modules/core/output_modules.php
@@ -1015,6 +1015,15 @@ public function test_content_section_end() {
$res = $test->run();
$this->assertEquals(array(''), $res->output_response);
}
+ /**
+ * @preserveGlobalState disabled
+ * @runInSeparateProcess
+ */
+ public function test_modals() {
+ $test = new Output_Test('modals', 'core');
+ $res = $test->run();
+ $this->assertEquals(array(''), $res->output_response);
+ }
/**
* @preserveGlobalState disabled
* @runInSeparateProcess