mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-25 01:13:25 +01:00
merge master
This commit is contained in:
commit
0cc7ce12d0
52
.travis.yml
52
.travis.yml
@ -6,32 +6,22 @@ php:
|
||||
# none of our dependencies allow 8.0
|
||||
# - master
|
||||
|
||||
matrix:
|
||||
os: linux
|
||||
|
||||
jobs:
|
||||
fast_finish: true
|
||||
# allow_failures:
|
||||
# - php: master
|
||||
|
||||
services:
|
||||
- memcached
|
||||
- mysql #we use mariadb instead installed via addons below
|
||||
- mysql
|
||||
# - postgres
|
||||
|
||||
#addons:
|
||||
# mariadb: '10.0'
|
||||
|
||||
sudo: required
|
||||
|
||||
before_script:
|
||||
- sudo apt-get update -qq
|
||||
- sudo apt-get install -y libpcre3-dev
|
||||
- sudo apt-get install -y libpcre3-dev apache2 libapache2-mod-fastcgi
|
||||
- case $(phpenv version-name) in
|
||||
"5.6")
|
||||
yes "" | pecl install memcache;
|
||||
yes "" | pecl install apcu-4.0.11;
|
||||
yes "" | pecl install igbinary;
|
||||
echo "extension=memcached.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini;
|
||||
phpenv config-rm xdebug.ini;
|
||||
;;
|
||||
"7"|"7.0"|"7.1"|"7.2")
|
||||
yes "" | pecl install apcu;
|
||||
echo "extension=memcached.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini;
|
||||
@ -41,17 +31,19 @@ before_script:
|
||||
echo "extension=memcached.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini;
|
||||
;;
|
||||
esac
|
||||
- case $(phpenv version-name) in
|
||||
"5.6")
|
||||
composer require 'phpunit/phpunit:~5.7';
|
||||
;;
|
||||
"7"|"7.0")
|
||||
composer require 'phpunit/phpunit:~6';
|
||||
;;
|
||||
*)
|
||||
composer require --ignore-platform-reqs 'phpunit/phpunit:~7';
|
||||
;;
|
||||
esac
|
||||
# enable apache with php-fpm see https://docs.travis-ci.com/user/languages/php/#apache--php
|
||||
- sudo cp ~/.phpenv/versions/$(phpenv version-name)/etc/php-fpm.conf.default ~/.phpenv/versions/$(phpenv version-name)/etc/php-fpm.conf
|
||||
- sudo a2enmod rewrite actions fastcgi alias
|
||||
- echo "cgi.fix_pathinfo = 1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
|
||||
- sudo sed -i -e "s,www-data,travis,g" /etc/apache2/envvars
|
||||
- sudo chown -R travis:travis /var/lib/apache2/fastcgi
|
||||
- ~/.phpenv/versions/$(phpenv version-name)/sbin/php-fpm
|
||||
# configure apache virtual hosts
|
||||
- sudo cp -f doc/travis-ci-apache.conf /etc/apache2/sites-available/000-default.conf
|
||||
- sudo sed -e "s?%TRAVIS_BUILD_DIR%?$(pwd)?g" --in-place /etc/apache2/sites-available/000-default.conf
|
||||
# remove .htaccess as it is read by Apache, but content is for mod_php
|
||||
- rm -f .htaccess
|
||||
- sudo service apache2 restart
|
||||
- php -m
|
||||
- php -i
|
||||
- php install-cli.php --ignore-platform-reqs
|
||||
@ -64,7 +56,7 @@ script:
|
||||
# install egroupware using MariaDB as domain "default"
|
||||
# and add an admin user "demo" with password "guest"
|
||||
- php doc/rpm-build/post_install.php --domain default
|
||||
--source_dir `pwd` --start_db '' --autostart_db '' --start_webserver '' --webserver_user ''
|
||||
--source_dir `pwd` --start_db '' --autostart_db '' --start_webserver '' --autostart_webserver '' --webserver_user ''
|
||||
--admin_user demo --admin_passwd guest --admin_email noreply@example.com
|
||||
# disable PostgreSQL install as it fails in Travis with Fatal error: Call to unimplemented native function pg_set_client_encoding
|
||||
# install egroupware using PostgreSQL as domain "pgsql", need some specific handling we can not create users via sql
|
||||
@ -79,8 +71,12 @@ script:
|
||||
# --source_dir `pwd` --start_db '' --autostart_db '' --start_webserver '' --webserver_user ''
|
||||
# Ubuntu has problems with #!/usr/bin/env php -dapc.enable=1, it stalls forever
|
||||
- vendor/bin/phpunit -c doc -dapc.enable_cli=1
|
||||
# output Apache error.log to diagnose PHP errors in requests send by unit-tests
|
||||
- echo "travis_fold:start:SCRIPT folding starts"
|
||||
- sudo cat /var/log/apache2/error.log
|
||||
# do not run syntax check for hhvm, as it always fails / get terminated after 10m
|
||||
- test $(phpenv version-name) = 'hhvm' || ./doc/php_syntax_check.sh
|
||||
- ./doc/php_syntax_check.sh
|
||||
- echo "travis_fold:start:SCRIPT folding ends"
|
||||
|
||||
cache:
|
||||
directories:
|
||||
|
@ -10,9 +10,9 @@
|
||||
*/
|
||||
|
||||
use EGroupware\Api;
|
||||
use EGroupware\Api\Framework;
|
||||
use EGroupware\Api\Acl;
|
||||
use EGroupware\Api\Etemplate;
|
||||
use EGroupware\Api\Framework;
|
||||
|
||||
/**
|
||||
* UI for admin: edit/add account
|
||||
@ -28,6 +28,13 @@ class admin_account
|
||||
'delete' => true,
|
||||
);
|
||||
|
||||
// Copying account uses addressbook fields, but we explicitly clear these
|
||||
protected static $copy_clear_fields = array(
|
||||
'account_firstname','account_lastname','account_fullname', 'person_id',
|
||||
'account_id','account_lid',
|
||||
'account_lastlogin','accountlastloginfrom','account_lastpwd_change'
|
||||
);
|
||||
|
||||
/**
|
||||
* Hook to edit account data via "Account" tab in addressbook edit dialog
|
||||
*
|
||||
@ -104,18 +111,24 @@ class admin_account
|
||||
}
|
||||
$readonlys['account_passwd'] = $readonlys['account_passwd2'] = true;
|
||||
}
|
||||
// save old values to only trigger save, if one of the following values change (contact data get saved anyway)
|
||||
$preserve = empty($content['id']) ? array() :
|
||||
array('old_account' => array_intersect_key($account, array_flip(array(
|
||||
'account_lid', 'account_status', 'account_groups', 'anonymous', 'changepassword',
|
||||
'mustchangepassword', 'account_primary_group', 'homedirectory', 'loginshell',
|
||||
'account_expires', 'account_firstname', 'account_lastname', 'account_email'))),
|
||||
'deny_edit' => $deny_edit);
|
||||
|
||||
if($content && $_GET['copy'])
|
||||
{
|
||||
$this->copy($content, $account, $preserve);
|
||||
}
|
||||
return array(
|
||||
'name' => 'admin.account',
|
||||
'prepend' => true,
|
||||
'label' => 'Account',
|
||||
'data' => $account,
|
||||
// save old values to only trigger save, if one of the following values change (contact data get saved anyway)
|
||||
'preserve' => empty($content['id']) ? array() :
|
||||
array('old_account' => array_intersect_key($account, array_flip(array(
|
||||
'account_lid', 'account_status', 'account_groups', 'anonymous', 'changepassword',
|
||||
'mustchangepassword', 'account_primary_group', 'homedirectory', 'loginshell',
|
||||
'account_expires', 'account_firstname', 'account_lastname', 'account_email'))),
|
||||
'deny_edit' => $deny_edit),
|
||||
'preserve' => $preserve,
|
||||
'readonlys' => $readonlys,
|
||||
'pre_save_callback' => $deny_edit ? null : 'admin_account::addressbook_pre_save',
|
||||
);
|
||||
@ -243,6 +256,35 @@ class admin_account
|
||||
}
|
||||
}
|
||||
|
||||
public function copy(array &$content, array &$account, array &$preserve)
|
||||
{
|
||||
// We skipped the addressbook copy, call it now
|
||||
$ab_ui = new addressbook_ui();
|
||||
$ab_ui->copy_contact($content, true);
|
||||
|
||||
// copy_contact() reset the owner, fix it
|
||||
$content['owner'] = '0';
|
||||
|
||||
// Explicitly, always clear these
|
||||
static $clear_content = Array(
|
||||
'n_family','n_given','n_middle','n_suffix','n_fn','n_fileas',
|
||||
'account_id','contact_id','id','etag','carddav_name','uid'
|
||||
);
|
||||
foreach($clear_content as $field)
|
||||
{
|
||||
$account[$field] ='';
|
||||
$preserve[$field] = '';
|
||||
}
|
||||
$account['link_to']['to_id'] = 0;
|
||||
unset($preserve['old_account']);
|
||||
|
||||
// Never copy these on an account
|
||||
foreach(static::$copy_clear_fields as $field)
|
||||
{
|
||||
unset($account[$field]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an account
|
||||
*
|
||||
|
@ -139,7 +139,10 @@ class admin_config
|
||||
$_POST = array('newsettings' => &$_content['newsettings']);
|
||||
|
||||
// Remove actual files (cleanup) of deselected urls from login_background_file
|
||||
$this->remove_anon_images(array_diff((array)$c->config_data['login_background_file'], $_content['newsettings']['login_background_file']));
|
||||
if (!empty($c->config_data['login_background_file']))
|
||||
{
|
||||
$this->remove_anon_images(array_diff((array)$c->config_data['login_background_file'], (array)$_content['newsettings']['login_background_file']));
|
||||
}
|
||||
|
||||
/* Load hook file with functions to validate each config (one/none/all) */
|
||||
$errors = Api\Hooks::single(array(
|
||||
|
@ -11,10 +11,10 @@
|
||||
*/
|
||||
|
||||
use EGroupware\Api;
|
||||
use EGroupware\Api\Link;
|
||||
use EGroupware\Api\Egw;
|
||||
use EGroupware\Api\Etemplate;
|
||||
use EGroupware\Api\Etemplate\Widget\Tree;
|
||||
use EGroupware\Api\Link;
|
||||
|
||||
/**
|
||||
* UI for admin
|
||||
@ -154,6 +154,13 @@ class admin_ui
|
||||
'onExecute' => 'javaScript:app.admin.account',
|
||||
'group' => $group,
|
||||
),
|
||||
'copy' => array(
|
||||
'caption' => 'Copy',
|
||||
'url' => 'menuaction=addressbook.addressbook_ui.edit&makecp=1&contact_id=$id',
|
||||
'onExecute' => 'javaScript:app.admin.account',
|
||||
'allowOnMultiple' => false,
|
||||
'icon' => 'copy',
|
||||
),
|
||||
);
|
||||
// generate urls for add/edit accounts via addressbook
|
||||
$edit = Link::get_registry('addressbook', 'edit');
|
||||
|
@ -760,11 +760,17 @@ var AdminApp = /** @class */ (function (_super) {
|
||||
AdminApp.prototype.account = function (_action, _senders) {
|
||||
var params = jQuery.extend({}, this.egw.link_get_registry('addressbook', 'edit'));
|
||||
var popup = this.egw.link_get_registry('addressbook', 'edit_popup');
|
||||
if (_action.id == 'add') {
|
||||
params.owner = '0';
|
||||
}
|
||||
else {
|
||||
params.account_id = _senders[0].id.split('::').pop(); // get last :: separated part
|
||||
switch (_action.id) {
|
||||
case 'add':
|
||||
params.owner = '0';
|
||||
break;
|
||||
case 'copy':
|
||||
params.owner = '0';
|
||||
params.copy = true;
|
||||
// Fall through
|
||||
default:
|
||||
params.account_id = _senders[0].id.split('::').pop(); // get last :: separated part
|
||||
break;
|
||||
}
|
||||
this.egw.open_link(this.egw.link('/index.php', params), 'admin', popup);
|
||||
};
|
||||
|
@ -888,14 +888,20 @@ class AdminApp extends EgwApp
|
||||
var params = jQuery.extend({}, this.egw.link_get_registry('addressbook', 'edit'));
|
||||
var popup = <string>this.egw.link_get_registry('addressbook', 'edit_popup');
|
||||
|
||||
if (_action.id == 'add')
|
||||
switch(_action.id)
|
||||
{
|
||||
params.owner = '0';
|
||||
}
|
||||
else
|
||||
{
|
||||
params.account_id = _senders[0].id.split('::').pop(); // get last :: separated part
|
||||
case 'add':
|
||||
params.owner = '0';
|
||||
break;
|
||||
case 'copy':
|
||||
params.owner = '0';
|
||||
params.copy = true;
|
||||
// Fall through
|
||||
default:
|
||||
params.account_id = _senders[0].id.split('::').pop(); // get last :: separated part
|
||||
break;
|
||||
}
|
||||
|
||||
this.egw.open_link(this.egw.link('/index.php', params), 'admin', popup);
|
||||
}
|
||||
|
||||
|
@ -3,58 +3,85 @@
|
||||
<!-- $Id$ -->
|
||||
<overlay>
|
||||
<template id="admin.account.delete.delete" template="" lang="" group="0" version="18.1.001">
|
||||
<vbox class="admin_account_delete">
|
||||
<description value="Who would you like to transfer records owned by the deleted user to?" class="dialogHeader2"/>
|
||||
<select-account id="new_owner" empty_label="Delete all records" class="dialogHeader3"/>
|
||||
<description value="Automatically transfer entries owned by the user:"/>
|
||||
<select id="delete_apps" rows="6" multiple="true" span="2"/>
|
||||
<description value="Please manually deal with entries owned by the user:"/>
|
||||
<grid id="counts" disabled="!@counts">
|
||||
<columns>
|
||||
<column width="150"/>
|
||||
<column/>
|
||||
</columns>
|
||||
<rows>
|
||||
<row>
|
||||
<select-app id="${row}[app]" readonly="true"/>
|
||||
<description id="${row}[count]"/>
|
||||
</row>
|
||||
</rows>
|
||||
</grid>
|
||||
<grid>
|
||||
<columns>
|
||||
<column width="150"/>
|
||||
<column/>
|
||||
</columns>
|
||||
<rows>
|
||||
<row>
|
||||
<select-app value="filemanager" readonly="true"/>
|
||||
<description value="Change owner of found files to the new user, and move the home folder to /home/new-user/old-home-username."/>
|
||||
</row>
|
||||
<row>
|
||||
<select-app value="mail" readonly="true"/>
|
||||
<description value="Please check email. It gets automatically deleted if email integration is used."/>
|
||||
</row>
|
||||
</rows>
|
||||
</grid>
|
||||
<description value="If you delete the user without selecting an account to move the data to, all entries get deleted!" font_style="b"/>
|
||||
</vbox>
|
||||
<grid width="100%" height="100%">
|
||||
<columns>
|
||||
<column width="60%"/>
|
||||
<column/>
|
||||
</columns>
|
||||
<rows>
|
||||
<row>
|
||||
<description value="Who would you like to transfer records owned by the deleted user to?" class="dialogHeader2"/>
|
||||
<select-account id="new_owner" empty_label="Delete all records" class="dialogHeader3"/>
|
||||
</row>
|
||||
<row>
|
||||
<vbox>
|
||||
<description value="Automatically transfer entries owned by the user:"/>
|
||||
<select id="delete_apps" rows="6" multiple="true" span="2"/>
|
||||
</vbox>
|
||||
</row>
|
||||
<row>
|
||||
<vbox>
|
||||
<description value="Please manually deal with entries owned by the user:"/>
|
||||
<grid id="counts" disabled="!@counts">
|
||||
<columns>
|
||||
<column width="150"/>
|
||||
<column/>
|
||||
</columns>
|
||||
<rows>
|
||||
<row>
|
||||
<select-app id="${row}[app]" readonly="true"/>
|
||||
<description id="${row}[count]"/>
|
||||
</row>
|
||||
</rows>
|
||||
</grid>
|
||||
<grid>
|
||||
<columns>
|
||||
<column width="150"/>
|
||||
<column/>
|
||||
</columns>
|
||||
<rows>
|
||||
<row>
|
||||
<select-app value="filemanager" readonly="true"/>
|
||||
<description value="Change owner of found files to the new user, and move the home folder to /home/new-user/old-home-username."/>
|
||||
</row>
|
||||
<row>
|
||||
<select-app value="mail" readonly="true"/>
|
||||
<description value="Please check email. It gets automatically deleted if email integration is used."/>
|
||||
</row>
|
||||
</rows>
|
||||
</grid>
|
||||
<description value="If you delete the user without selecting an account to move the data to, all entries get deleted!" font_style="b"/>
|
||||
</vbox>
|
||||
</row>
|
||||
</rows>
|
||||
</grid>
|
||||
</template>
|
||||
<template id="admin.account.delete" template="" lang="" group="0" version="18.1.001">
|
||||
<box class="dialogHeader">
|
||||
<select-account id="account_id" readonly="true" label="Delete" onchange="var apps = widget.getRoot().getWidgetById('delete_apps'); apps.set_enabled(widget.getValue());"/>
|
||||
</box>
|
||||
<tabbox id="tabs" width="99%" tab_height="400px">
|
||||
<tabs>
|
||||
<tab id="main" label="Delete"/>
|
||||
</tabs>
|
||||
<tabpanels>
|
||||
<template template="admin.account.delete.delete" width="99%"/>
|
||||
</tabpanels>
|
||||
</tabbox>
|
||||
<hbox class="dialogFooterToolbar">
|
||||
<button id="delete" label="Delete"/>
|
||||
<button id="cancel" label="Cancel" onclick="window.close()"/>
|
||||
</hbox>
|
||||
<grid width="100%">
|
||||
<columns>
|
||||
<column width="100%"/>
|
||||
</columns>
|
||||
<rows>
|
||||
<row class="dialogHeader">
|
||||
<select-account id="account_id" readonly="true" label="Delete" onchange="var apps = widget.getRoot().getWidgetById('delete_apps'); apps.set_enabled(widget.getValue());"/>
|
||||
</row>
|
||||
<row>
|
||||
<tabbox id="tabs" width="100%" tab_height="400px">
|
||||
<tabs>
|
||||
<tab id="main" label="Delete"/>
|
||||
</tabs>
|
||||
<tabpanels>
|
||||
<template template="admin.account.delete.delete" width="100%"/>
|
||||
</tabpanels>
|
||||
</tabbox>
|
||||
</row>
|
||||
<row class="dialogFooterToolbar">
|
||||
<hbox>
|
||||
<button id="delete" label="Delete"/>
|
||||
<button id="cancel" label="Cancel" onclick="window.close()"/>
|
||||
</hbox>
|
||||
</row>
|
||||
</rows>
|
||||
</grid>
|
||||
</template>
|
||||
</overlay>
|
||||
|
@ -58,10 +58,7 @@ td.admin_userAgent span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.admin_account_delete > * {
|
||||
padding: 8px;
|
||||
}
|
||||
form#admin-account-delete div.dialogHeader > label {
|
||||
#admin-account-delete .dialogHeader label.et2_label {
|
||||
font-size: 120%;
|
||||
}
|
||||
/* Global Category classes*/
|
||||
|
@ -66,10 +66,7 @@ td.admin_userAgent span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.admin_account_delete > * {
|
||||
padding: 8px;
|
||||
}
|
||||
form#admin-account-delete div.dialogHeader > label {
|
||||
#admin-account-delete .dialogHeader label.et2_label {
|
||||
font-size: 120%;
|
||||
}
|
||||
/* Global Category classes*/
|
||||
@ -249,4 +246,7 @@ Admin command
|
||||
filter: initial;
|
||||
/* IE 6-9 */
|
||||
}
|
||||
#admin-account-delete .dialogFooterToolbar .et2_button_delete {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
@ -59,4 +59,8 @@
|
||||
/*filter grey*/
|
||||
.img_filter_none;
|
||||
}
|
||||
|
||||
#admin-account-delete .dialogFooterToolbar .et2_button_delete {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ class AclCommandTest extends CommandBase {
|
||||
/**
|
||||
* Create accounts for testing
|
||||
*/
|
||||
public function setUp()
|
||||
protected function setUp() : void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
@ -74,7 +74,7 @@ class AclCommandTest extends CommandBase {
|
||||
$this->assertNotEmpty($this->account_id, 'Did not create test user account');
|
||||
}
|
||||
|
||||
public function tearDown()
|
||||
protected function tearDown() : void
|
||||
{
|
||||
// Delete the accounts we created
|
||||
if($this->group_id)
|
||||
|
@ -23,7 +23,7 @@ class ConfigCommandTest extends CommandBase
|
||||
// If we add a config, make sure we can delete it for clean up
|
||||
protected $config_name = 'test_config';
|
||||
|
||||
public function tearDown()
|
||||
protected function tearDown() : void
|
||||
{
|
||||
if($this->config_name)
|
||||
{
|
||||
|
@ -29,7 +29,7 @@ class DeleteAccountCommandTest extends CommandBase {
|
||||
'account_lastname' => 'Test'
|
||||
);
|
||||
|
||||
public function setUp()
|
||||
protected function setUp() : void
|
||||
{
|
||||
if(($account_id = $GLOBALS['egw']->accounts->name2id($this->account['account_lid'])))
|
||||
{
|
||||
@ -44,7 +44,7 @@ class DeleteAccountCommandTest extends CommandBase {
|
||||
$this->assertNotEmpty($this->account_id, 'Did not create test user account');
|
||||
}
|
||||
|
||||
public function tearDown()
|
||||
protected function tearDown() : void
|
||||
{
|
||||
if($this->account_id && ($GLOBALS['egw']->accounts->id2name($this->account_id)))
|
||||
{
|
||||
|
@ -28,7 +28,7 @@ class GroupCommandTest extends CommandBase {
|
||||
'account_members' => array()
|
||||
);
|
||||
|
||||
public function setUp()
|
||||
protected function setUp() : void
|
||||
{
|
||||
// Can't set this until now - value is not available
|
||||
$this->group['account_members'] = array($GLOBALS['egw_info']['user']['account_id']);
|
||||
@ -39,7 +39,7 @@ class GroupCommandTest extends CommandBase {
|
||||
$GLOBALS['egw']->accounts->delete($account_id);
|
||||
}
|
||||
}
|
||||
public function tearDown()
|
||||
protected function tearDown() : void
|
||||
{
|
||||
// Delete the accounts we created
|
||||
if($this->group_id)
|
||||
|
@ -23,13 +23,13 @@ class PreferencesCommandTest extends CommandBase
|
||||
// If we add a preference, make sure we can delete it for clean up
|
||||
protected $preference_name = 'test_preference';
|
||||
|
||||
public function setUp()
|
||||
protected function setUp() : void
|
||||
{
|
||||
Api\Cache::unsetInstance(Api\Preferences::class, 'forced');
|
||||
Api\Cache::unsetInstance(Api\Preferences::class, 'default');
|
||||
Api\Cache::unsetInstance(Api\Preferences::class, $GLOBALS['egw_info']['user']['account_id']);
|
||||
}
|
||||
public function tearDown()
|
||||
protected function tearDown() : void
|
||||
{
|
||||
if($this->preference_name)
|
||||
{
|
||||
|
@ -29,7 +29,7 @@ class UserCommandTest extends CommandBase {
|
||||
'account_lastname' => 'Test'
|
||||
);
|
||||
|
||||
public function tearDown()
|
||||
protected function tearDown() : void
|
||||
{
|
||||
if($this->account_id)
|
||||
{
|
||||
|
@ -114,7 +114,7 @@ var et2_htmlarea = /** @class */ (function (_super) {
|
||||
valid_children: this.options.valid_children,
|
||||
plugins: [
|
||||
"print searchreplace autolink directionality ",
|
||||
"visualblocks visualchars image link media template ",
|
||||
"visualblocks visualchars image link media template fullscreen",
|
||||
"codesample table charmap hr pagebreak nonbreaking anchor toc ",
|
||||
"insertdatetime advlist lists textcolor wordcount imagetools ",
|
||||
"colorpicker textpattern help paste code searchreplace tabfocus"
|
||||
@ -440,14 +440,14 @@ var et2_htmlarea = /** @class */ (function (_super) {
|
||||
*/
|
||||
et2_htmlarea.TOOLBAR_EXTENDED = "fontselect fontsizeselect | bold italic strikethrough forecolor backcolor | " +
|
||||
"link | alignleft aligncenter alignright alignjustify | numlist " +
|
||||
"bullist outdent indent | removeformat | image";
|
||||
"bullist outdent indent | removeformat | image | fullscreen";
|
||||
/**
|
||||
* arranged toolbars as advanced mode
|
||||
* @constant
|
||||
*/
|
||||
et2_htmlarea.TOOLBAR_ADVANCED = "undo redo| formatselect | fontselect fontsizeselect | bold italic strikethrough forecolor backcolor | " +
|
||||
"link | alignleft aligncenter alignright alignjustify | numlist " +
|
||||
"bullist outdent indent ltr rtl | removeformat code| image | searchreplace";
|
||||
"bullist outdent indent ltr rtl | removeformat code| image | searchreplace | fullscreen";
|
||||
/**
|
||||
* font size formats
|
||||
* @constant
|
||||
|
@ -110,7 +110,7 @@ class et2_htmlarea extends et2_editableWidget implements et2_IResizeable
|
||||
*/
|
||||
public static readonly TOOLBAR_EXTENDED : string = "fontselect fontsizeselect | bold italic strikethrough forecolor backcolor | "+
|
||||
"link | alignleft aligncenter alignright alignjustify | numlist "+
|
||||
"bullist outdent indent | removeformat | image";
|
||||
"bullist outdent indent | removeformat | image | fullscreen";
|
||||
|
||||
/**
|
||||
* arranged toolbars as advanced mode
|
||||
@ -118,7 +118,7 @@ class et2_htmlarea extends et2_editableWidget implements et2_IResizeable
|
||||
*/
|
||||
public static readonly TOOLBAR_ADVANCED : string = "undo redo| formatselect | fontselect fontsizeselect | bold italic strikethrough forecolor backcolor | "+
|
||||
"link | alignleft aligncenter alignright alignjustify | numlist "+
|
||||
"bullist outdent indent ltr rtl | removeformat code| image | searchreplace";
|
||||
"bullist outdent indent ltr rtl | removeformat code| image | searchreplace | fullscreen";
|
||||
|
||||
/**
|
||||
* font size formats
|
||||
@ -232,7 +232,7 @@ class et2_htmlarea extends et2_editableWidget implements et2_IResizeable
|
||||
valid_children : this.options.valid_children,
|
||||
plugins: [
|
||||
"print searchreplace autolink directionality ",
|
||||
"visualblocks visualchars image link media template ",
|
||||
"visualblocks visualchars image link media template fullscreen",
|
||||
"codesample table charmap hr pagebreak nonbreaking anchor toc ",
|
||||
"insertdatetime advlist lists textcolor wordcount imagetools ",
|
||||
"colorpicker textpattern help paste code searchreplace tabfocus"
|
||||
|
@ -266,8 +266,8 @@ var etemplate2 = /** @class */ (function () {
|
||||
etemplate2.prototype.bind_unload = function () {
|
||||
if (this._etemplate_exec_id) {
|
||||
this.destroy_session = jQuery.proxy(function (ev) {
|
||||
var request = egw.json("EGroupware\\Api\\Etemplate::ajax_destroy_session", [this._etemplate_exec_id], null, null, false);
|
||||
request.sendRequest();
|
||||
// need to use async === "keepalive" to run via beforeunload
|
||||
egw.json("EGroupware\\Api\\Etemplate::ajax_destroy_session", [this._etemplate_exec_id], null, null, "keepalive").sendRequest();
|
||||
}, this);
|
||||
if (!window.onbeforeunload) {
|
||||
window.onbeforeunload = this.destroy_session;
|
||||
|
@ -335,9 +335,9 @@ export class etemplate2
|
||||
{
|
||||
this.destroy_session = jQuery.proxy(function (ev)
|
||||
{
|
||||
const request = egw.json("EGroupware\\Api\\Etemplate::ajax_destroy_session",
|
||||
[this._etemplate_exec_id], null, null, false);
|
||||
request.sendRequest();
|
||||
// need to use async === "keepalive" to run via beforeunload
|
||||
egw.json("EGroupware\\Api\\Etemplate::ajax_destroy_session",
|
||||
[this._etemplate_exec_id], null, null, "keepalive").sendRequest();
|
||||
}, this);
|
||||
|
||||
if (!window.onbeforeunload)
|
||||
|
11
api/js/jsapi/egw_global.d.ts
vendored
11
api/js/jsapi/egw_global.d.ts
vendored
@ -673,13 +673,14 @@ declare class JsonRequest
|
||||
{
|
||||
/**
|
||||
* Sends the assembled request to the server
|
||||
* @param {boolean} [async=false] Overrides async provided in constructor to give an easy way to make simple async requests
|
||||
* @param {boolean|"keepalive"} _async true: asynchronious request, false: synchronious request,
|
||||
* "keepalive": async. request with keepalive===true / sendBeacon, to be used in beforeunload event
|
||||
* @param {string} method ='POST' allow to eg. use a (cachable) 'GET' request instead of POST
|
||||
* @param {function} error option error callback(_xmlhttp, _err) used instead our default this.error
|
||||
*
|
||||
* @return {jqXHR} jQuery jqXHR request object
|
||||
*/
|
||||
sendRequest(async? : boolean, method? : "POST"|"GET", error? : Function)
|
||||
sendRequest(async? : boolean|"keepalive", method? : "POST"|"GET", error? : Function)
|
||||
/**
|
||||
* Open websocket to push server (and keeps it open)
|
||||
*
|
||||
@ -721,14 +722,14 @@ declare interface IegwWndLocal extends IegwGlobal
|
||||
* which handles the actual request. If the menuaction is a full featured
|
||||
* url, this one will be used instead.
|
||||
* @param _parameters which should be passed to the menuaction function.
|
||||
* @param _async specifies whether the request should be asynchronous or
|
||||
* not.
|
||||
* @param {boolean|"keepalive"} _async true: asynchronious request, false: synchronious request,
|
||||
* "keepalive": async. request with keepalive===true / sendBeacon, to be used in beforeunload event
|
||||
* @param _callback specifies the callback function which should be
|
||||
* called, once the request has been sucessfully executed.
|
||||
* @param _context is the context which will be used for the callback function
|
||||
* @param _sender is a parameter being passed to the _callback function
|
||||
*/
|
||||
json(_menuaction : string, _parameters? : any[], _callback? : Function, _context? : object, _async? : boolean, _sender?) : JsonRequest;
|
||||
json(_menuaction : string, _parameters? : any[], _callback? : Function, _context? : object, _async? : boolean|"keepalive", _sender?) : JsonRequest;
|
||||
/**
|
||||
* Registers a new handler plugin.
|
||||
*
|
||||
|
@ -7,7 +7,6 @@
|
||||
* @link http://www.egroupware.org
|
||||
* @author Andreas Stöckel (as AT stylite.de)
|
||||
* @author Ralf Becker <RalfBecker@outdoor-training.de>
|
||||
* @version $Id$
|
||||
*/
|
||||
|
||||
/*egw:uses
|
||||
@ -53,7 +52,8 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd)
|
||||
* @param {array} _parameters
|
||||
* @param {function} _callback
|
||||
* @param {object} _context
|
||||
* @param {boolean} _async
|
||||
* @param {boolean|"keepalive"} _async true: asynchronious request, false: synchronious request,
|
||||
* "keepalive": async. request with keepalive===true / sendBeacon, to be used in boforeunload event
|
||||
* @param {object} _sender
|
||||
* @param {egw} _egw
|
||||
*/
|
||||
@ -154,11 +154,12 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd)
|
||||
|
||||
/**
|
||||
* Sends the assembled request to the server
|
||||
* @param {boolean} [async=false] Overrides async provided in constructor to give an easy way to make simple async requests
|
||||
* @param {boolean|"keepalive"} _async Overrides async provided in constructor: true: asynchronious request,
|
||||
* false: synchronious request, "keepalive": async. request with keepalive===true / sendBeacon, to be used in beforeunload event
|
||||
* @param {string} method ='POST' allow to eg. use a (cachable) 'GET' request instead of POST
|
||||
* @param {function} error option error callback(_xmlhttp, _err) used instead our default this.error
|
||||
*
|
||||
* @return {jqXHR} jQuery jqXHR request object
|
||||
* @return {jqXHR|boolean} jQuery jqXHR request object or for async==="keepalive" boolean is returned
|
||||
*/
|
||||
json_request.prototype.sendRequest = function(async, method, error)
|
||||
{
|
||||
@ -176,6 +177,15 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd)
|
||||
}
|
||||
});
|
||||
|
||||
// send with keepalive===true or sendBeacon to be used in beforeunload event
|
||||
if (this.async === "keepalive" && typeof navigator.sendBeacon !== "undefined")
|
||||
{
|
||||
const data = new FormData();
|
||||
data.append('json_data', request_obj);
|
||||
//(window.opener||window).console.log("navigator.sendBeacon", this.url, request_obj, data.getAll('json_data'));
|
||||
return navigator.sendBeacon(this.url, data);
|
||||
}
|
||||
|
||||
// Send the request via AJAX using the jquery ajax function
|
||||
// we need to use jQuery of window of egw object, as otherwise the one from main window is used!
|
||||
// (causing eg. apply from server with app.$app.method to run in main window instead of popup)
|
||||
|
@ -983,7 +983,7 @@ class Accounts
|
||||
if (!is_array($app_users))
|
||||
{
|
||||
self::setup_cache();
|
||||
$cache = &self::$cache['account_split'][$app_user];
|
||||
$cache = &self::$cache['account_split'][$app_users];
|
||||
|
||||
if (is_array($cache))
|
||||
{
|
||||
|
@ -125,7 +125,7 @@ class Ldap
|
||||
*
|
||||
* @var Api\Accounts
|
||||
*/
|
||||
private $frontend;
|
||||
protected $frontend;
|
||||
|
||||
/**
|
||||
* Instance of the ldap class
|
||||
|
@ -769,9 +769,10 @@ class Cache
|
||||
if (is_null(Cache::$default_provider))
|
||||
{
|
||||
Cache::$default_provider =
|
||||
function_exists('apcu_fetch') && Cache\Apcu::available() ? 'EGroupware\Api\Cache\Apcu' :
|
||||
(function_exists('apc_fetch') && Cache\Apc::available() ? 'EGroupware\Api\Cache\Apc' :
|
||||
'EGroupware\Api\Cache\Files');
|
||||
PHP_SAPI === 'cli' ? 'EGroupware\Api\Cache\Files' :
|
||||
(function_exists('apcu_fetch') && Cache\Apcu::available() ? 'EGroupware\Api\Cache\Apcu' :
|
||||
(function_exists('apc_fetch') && Cache\Apc::available() ? 'EGroupware\Api\Cache\Apc' :
|
||||
'EGroupware\Api\Cache\Files'));
|
||||
}
|
||||
|
||||
//error_log('Cache::$default_provider='.array2string(Cache::$default_provider));
|
||||
|
@ -13,6 +13,9 @@
|
||||
|
||||
namespace EGroupware\Api\Cache;
|
||||
|
||||
// fix warning in tests, if memcache extension not available
|
||||
if (!defined('MEMCACHE_COMPRESSED')) define('MEMCACHE_COMPRESSED', 2);
|
||||
|
||||
/**
|
||||
* Caching provider storing data in memcached via PHP's memcache extension
|
||||
*
|
||||
|
@ -548,13 +548,13 @@ abstract class Framework extends Framework\Extra
|
||||
$var['logo_header'] = self::get_login_logo_or_bg_url('login_logo_header', 'logo');
|
||||
}
|
||||
|
||||
$var['logo_url'] = $GLOBALS['egw_info']['server']['login_logo_url']?$GLOBALS['egw_info']['server']['login_logo_url']:'http://www.eGroupWare.org';
|
||||
$var['logo_url'] = $GLOBALS['egw_info']['server']['login_logo_url']?$GLOBALS['egw_info']['server']['login_logo_url']:'http://www.egroupware.org';
|
||||
|
||||
if (substr($var['logo_url'],0,4) != 'http')
|
||||
{
|
||||
$var['logo_url'] = 'http://'.$var['logo_url'];
|
||||
}
|
||||
$var['logo_title'] = $GLOBALS['egw_info']['server']['login_logo_title']?$GLOBALS['egw_info']['server']['login_logo_title']:'www.eGroupWare.org';
|
||||
$var['logo_title'] = $GLOBALS['egw_info']['server']['login_logo_title']?$GLOBALS['egw_info']['server']['login_logo_title']:'www.egroupware.org';
|
||||
|
||||
return $var;
|
||||
}
|
||||
|
@ -98,6 +98,9 @@ class Sieve extends Horde\ManageSieve
|
||||
//'logger' => new \admin_mail_logger('/tmp/sieve.log'),
|
||||
);
|
||||
}
|
||||
// try "PLAIN" first, in case IMAP wrongly reports some digest, it does not (correctly) implement
|
||||
array_unshift($this->supportedAuthMethods, self::AUTH_PLAIN);
|
||||
|
||||
parent::__construct($params);
|
||||
|
||||
$this->displayCharset = Translation::charset();
|
||||
|
@ -252,7 +252,13 @@ class Session
|
||||
$config->value('num_unsuccessful_ip',$GLOBALS['egw_info']['server']['num_unsuccessful_ip']);
|
||||
$config->value('install_id',$GLOBALS['egw_info']['server']['install_id']);
|
||||
$config->value('max_history',$GLOBALS['egw_info']['server']['max_history']);
|
||||
$config->save_repository();
|
||||
try
|
||||
{
|
||||
$config->save_repository();
|
||||
}
|
||||
catch (Db\Exception $e) {
|
||||
_egw_log_exception($e); // ignore exception, as it blocks session creation, if database is not writable
|
||||
}
|
||||
}
|
||||
}
|
||||
self::set_cookiedomain();
|
||||
@ -544,9 +550,12 @@ class Session
|
||||
// --> allows this stateless protocolls which use basic auth to use sessions!
|
||||
if (($this->sessionid = self::get_sessionid(true)))
|
||||
{
|
||||
session_id($this->sessionid);
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) // gives warning including password
|
||||
{
|
||||
session_id($this->sessionid);
|
||||
}
|
||||
}
|
||||
else
|
||||
elseif (!headers_sent()) // only gives warnings, nothing we can do
|
||||
{
|
||||
self::cache_control();
|
||||
session_start();
|
||||
@ -557,6 +566,10 @@ class Session
|
||||
}
|
||||
$this->sessionid = session_id();
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->sessionid = session_id() ?: Auth::randomstring(24);
|
||||
}
|
||||
$this->kp3 = Auth::randomstring(24);
|
||||
|
||||
$GLOBALS['egw_info']['user'] = $this->read_repositories();
|
||||
@ -635,26 +648,30 @@ class Session
|
||||
}
|
||||
$GLOBALS['egw']->db->transaction_commit();
|
||||
|
||||
if ($GLOBALS['egw_info']['server']['usecookies'] && !$no_session)
|
||||
if (!headers_sent())
|
||||
{
|
||||
self::egw_setcookie(self::EGW_SESSION_NAME,$this->sessionid);
|
||||
self::egw_setcookie('kp3',$this->kp3);
|
||||
self::egw_setcookie('domain',$this->account_domain);
|
||||
}
|
||||
if ($GLOBALS['egw_info']['server']['usecookies'] && !$no_session || isset($_COOKIE['last_loginid']))
|
||||
{
|
||||
self::egw_setcookie('last_loginid', $this->account_lid ,$now+1209600); /* For 2 weeks */
|
||||
self::egw_setcookie('last_domain',$this->account_domain,$now+1209600);
|
||||
}
|
||||
if ($GLOBALS['egw_info']['server']['usecookies'] && !$no_session)
|
||||
{
|
||||
self::egw_setcookie(self::EGW_SESSION_NAME, $this->sessionid);
|
||||
self::egw_setcookie('kp3', $this->kp3);
|
||||
self::egw_setcookie('domain', $this->account_domain);
|
||||
}
|
||||
if ($GLOBALS['egw_info']['server']['usecookies'] && !$no_session || isset($_COOKIE['last_loginid']))
|
||||
{
|
||||
self::egw_setcookie('last_loginid', $this->account_lid, $now + 1209600); /* For 2 weeks */
|
||||
self::egw_setcookie('last_domain', $this->account_domain, $now + 1209600);
|
||||
}
|
||||
|
||||
// set new remember me token/cookie, if requested and necessary
|
||||
$expiration = null;
|
||||
if (($token = $this->checkSetRememberMeToken($remember_me, $_COOKIE[self::REMEMBER_ME_COOKIE], $expiration)))
|
||||
{
|
||||
self::egw_setcookie(self::REMEMBER_ME_COOKIE, $token, $expiration);
|
||||
}
|
||||
// set new remember me token/cookie, if requested and necessary
|
||||
$expiration = null;
|
||||
if (($token = $this->checkSetRememberMeToken($remember_me, $_COOKIE[self::REMEMBER_ME_COOKIE], $expiration)))
|
||||
{
|
||||
self::egw_setcookie(self::REMEMBER_ME_COOKIE, $token, $expiration);
|
||||
}
|
||||
|
||||
if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."($this->login,$this->passwd,$this->passwd_type,$no_session,$auth_check) successfull sessionid=$this->sessionid");
|
||||
if (self::ERROR_LOG_DEBUG) error_log(__METHOD__ . "($this->login,$this->passwd,$this->passwd_type,$no_session,$auth_check) successfull sessionid=$this->sessionid");
|
||||
}
|
||||
elseif (self::ERROR_LOG_DEBUG) error_log(__METHOD__ . "($this->login,$this->passwd,$this->passwd_type,$no_session,$auth_check) could NOT set session cookies, headers already sent");
|
||||
|
||||
// hook called once session is created
|
||||
Hooks::process(array(
|
||||
@ -1649,6 +1666,8 @@ class Session
|
||||
*/
|
||||
private static function set_cookiedomain()
|
||||
{
|
||||
if (PHP_SAPI === "cli") return; // gives warnings and has no benefit
|
||||
|
||||
if ($GLOBALS['egw_info']['server']['cookiedomain'])
|
||||
{
|
||||
// Admin set domain, eg. .domain.com to allow egw.domain.com and www.domain.com
|
||||
@ -1957,6 +1976,7 @@ class Session
|
||||
case PHP_SESSION_DISABLED:
|
||||
throw new \ErrorException('EGroupware requires PHP session extension!');
|
||||
case PHP_SESSION_NONE:
|
||||
if (headers_sent()) return false; // only gives warnings
|
||||
ini_set('session.use_cookies',0); // disable the automatic use of cookies, as it uses the path / by default
|
||||
session_name(self::EGW_SESSION_NAME);
|
||||
if (($sessionid = self::get_sessionid()))
|
||||
|
@ -207,7 +207,7 @@ class Sharing
|
||||
header('WWW-Authenticate: Basic realm="'.$realm.'"');
|
||||
return static::share_fail(
|
||||
'401 Unauthorized',
|
||||
"<html>\n<head>\n<title>401 Unauthorized</title>\n<body>\nAuthorization failed.\n</body>\n</html>\n"
|
||||
"Authorization failed."
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -395,7 +395,11 @@ class Translation
|
||||
static function &load_app($app,$lang)
|
||||
{
|
||||
//$start = microtime(true);
|
||||
if (is_null(self::$db)) self::init(false);
|
||||
if (!isset(self::$db))
|
||||
{
|
||||
self::init(false);
|
||||
if (!isset(self::$db)) return;
|
||||
}
|
||||
$loaded = array();
|
||||
foreach(self::$db->select(self::LANG_TABLE,'message_id,content',array(
|
||||
'lang' => $lang,
|
||||
|
@ -90,9 +90,8 @@ class Sharing extends \EGroupware\Api\Sharing
|
||||
*/
|
||||
public static function setup_share($keep_session, &$share)
|
||||
{
|
||||
|
||||
// need to reset fs_tab, as resolve_url does NOT work with just share mounted
|
||||
if (count($GLOBALS['egw_info']['server']['vfs_fstab']) <= 1)
|
||||
if (empty($GLOBALS['egw_info']['server']['vfs_fstab']) || count($GLOBALS['egw_info']['server']['vfs_fstab']) <= 1)
|
||||
{
|
||||
unset($GLOBALS['egw_info']['server']['vfs_fstab']); // triggers reset of fstab in mount()
|
||||
$GLOBALS['egw_info']['server']['vfs_fstab'] = Vfs::mount();
|
||||
|
@ -367,7 +367,7 @@ function function_backtrace($remove=0)
|
||||
return $_GET['menuaction'] ? $_GET['menuaction'] : str_replace(EGW_SERVER_ROOT,'',$_SERVER['SCRIPT_FILENAME']);
|
||||
}
|
||||
|
||||
if (!function_exists('lang') || defined('NO_LANG')) // setup declares an own version
|
||||
if (!function_exists('lang') && !defined('NO_LANG')) // setup declares an own version
|
||||
{
|
||||
/**
|
||||
* function to handle multilanguage support
|
||||
|
377
api/tests/CalDAVTest.php
Normal file
377
api/tests/CalDAVTest.php
Normal file
@ -0,0 +1,377 @@
|
||||
<?php
|
||||
/**
|
||||
* CalDAV tests base class
|
||||
*
|
||||
* @link http://www.egroupware.org
|
||||
* @author Ralf Becker
|
||||
* @package api
|
||||
* @subpackage caldav
|
||||
* @copyright (c) 2020 by Ralf Becker <rb@egroupware.org>
|
||||
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
||||
*/
|
||||
|
||||
namespace EGroupware\Api;
|
||||
|
||||
// so tests can run standalone
|
||||
require_once __DIR__.'/../src/loader/common.php'; // autoloader
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use GuzzleHttp\Client, GuzzleHttp\RequestOptions;
|
||||
use Horde_Icalendar, Horde_Icalendar_Exception;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Abstract CalDAVTest using GuzzleHttp\Client against EGroupware CalDAV/CardDAV server
|
||||
*
|
||||
* @see http://docs.guzzlephp.org/en/v6/quickstart.html
|
||||
*
|
||||
* @package EGroupware\Api
|
||||
*/
|
||||
abstract class CalDAVTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Base URL of CalDAV server
|
||||
*/
|
||||
const CALDAV_BASE = 'http://localhost/egroupware/groupdav.php';
|
||||
|
||||
/**
|
||||
* Get full URL for a CalDAV path
|
||||
*
|
||||
* @param string $path CalDAV path
|
||||
* @return string URL
|
||||
*/
|
||||
protected function url($path='/')
|
||||
{
|
||||
$base = self::CALDAV_BASE;
|
||||
if (!empty($GLOBALS['EGW_DOMAIN']) && $GLOBALS['EGW_DOMAIN'] !== 'default')
|
||||
{
|
||||
$base = str_replace('localhost', $GLOBALS['EGW_DOMAIN'], $base);
|
||||
}
|
||||
return $base.$path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default options for GuzzleHttp\Client
|
||||
*
|
||||
* @var array
|
||||
* @see http://docs.guzzlephp.org/en/v6/request-options.html
|
||||
*/
|
||||
protected $client_options = [
|
||||
RequestOptions::HTTP_ERRORS => false, // return all HTTP status, not throwing exceptions
|
||||
RequestOptions::HEADERS => [
|
||||
'Cookie' => 'XDEBUG_SESSION=PHPSTORM',
|
||||
//'User-Agent' => 'CalDAVSynchronizer',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Get HTTP client for tests
|
||||
*
|
||||
* It will use by default the always existing user "demo" with password "guest" (use [] to NOT authenticate).
|
||||
* Additional users need to be created with $this->createUser("name").
|
||||
*
|
||||
* @param string|array $user_or_options ='demo' string with account_lid of user for authentication or array of options
|
||||
* @return Client
|
||||
* @see http://docs.guzzlephp.org/en/v6/request-options.html
|
||||
* @see http://docs.guzzlephp.org/en/v6/quickstart.html
|
||||
*/
|
||||
protected function getClient($user_or_options='demo')
|
||||
{
|
||||
if (!is_array($user_or_options))
|
||||
{
|
||||
$user_or_options = $this->auth($user_or_options);
|
||||
}
|
||||
return new Client(array_merge($this->client_options, $user_or_options));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a number of users with optional ACL rights too
|
||||
*
|
||||
* Example with boss granting secretary full rights on his calendar, plus one other user:
|
||||
*
|
||||
* $users = [
|
||||
* 'boss' => [],
|
||||
* 'secretary' => ['rights' => ['boss' => Acl::READ|Acl::ADD|Acl::EDIT|Acl::DELETE]],
|
||||
* 'other' => [],
|
||||
* ];
|
||||
* self::createUsersACL($users);
|
||||
*
|
||||
* @param array& $users $account_lid => array with values for keys with (defaults) "firstname" ($_acount_lid), "lastname" ("User"),
|
||||
* "email" ("$_account_lid@example.org"), "password" (random string), "primary_group" ("NoGroups" to not set rights)
|
||||
* "rights" array with $grantee => $rights pairs (need to be created before!)
|
||||
* @param string $app app to create the rights for, default "calendar"
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function createUsersACL(array &$users, $app='calendar')
|
||||
{
|
||||
foreach($users as $user => $data)
|
||||
{
|
||||
$data['id'] = self::createUser($user, $data);
|
||||
|
||||
foreach($data['rights'] ?? [] as $grantee => $rights)
|
||||
{
|
||||
self::addAcl('calendar', $data['id'], $grantee, $rights);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Array to track created users for tearDown and authentication
|
||||
*
|
||||
* @var array $account_lid => array with other data pairs
|
||||
*/
|
||||
private static $created_users = [];
|
||||
|
||||
/**
|
||||
* Create a user
|
||||
*
|
||||
* Created users are automatic deleted in tearDown() and can be passed to auth() or getClient() methods.
|
||||
* Users have random passwords to force new/different sessions!
|
||||
*
|
||||
* @param string $_account_lid
|
||||
* @param array& $data =[] values for keys with (defaults) "firstname" ($_acount_lid), "lastname" ("User"),
|
||||
* "email" ("$_account_lid@example.org"), "password" (random string), "primary_group" ("NoGroups" to not set rights)
|
||||
* on return: with defaults set
|
||||
* @return int account_id of created user
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected static function createUser($_account_lid, array &$data=[])
|
||||
{
|
||||
// add some defaults
|
||||
$data = array_merge([
|
||||
'firstname' => ucfirst($_account_lid),
|
||||
'lastname' => 'User',
|
||||
'email' => $_account_lid.'@example.org',
|
||||
'password' => 'secret',//Auth::randomstring(12),
|
||||
'primary_group' => 'NoGroup',
|
||||
], $data);
|
||||
|
||||
$data['id'] = self::getSetup()->add_account($_account_lid, $data['firstname'], $data['lastname'],
|
||||
$data['password'], $data['primary_group'], false, $data['email']);
|
||||
|
||||
// give use run rights for CalDAV apps, as NoGroup does NOT!
|
||||
self::addAcl(['groupdav','calendar','infolog','addressbook'], 'run', $data['id']);
|
||||
|
||||
self::$created_users[$_account_lid] = $data;
|
||||
|
||||
return $data['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authentication information for given user to use
|
||||
*
|
||||
* @param string $_account_lid ='demo'
|
||||
* @return array
|
||||
*/
|
||||
protected function auth($_account_lid='demo')
|
||||
{
|
||||
if ($_account_lid === 'demo')
|
||||
{
|
||||
$password = 'guest';
|
||||
}
|
||||
elseif (!isset(self::$created_users[$_account_lid]))
|
||||
{
|
||||
throw new \InvalidArgumentException("No user '$_account_lid' exist, need to create it with createUser('$_account_lid')");
|
||||
}
|
||||
else
|
||||
{
|
||||
$password = self::$created_users[$_account_lid]['password'];
|
||||
}
|
||||
return [RequestOptions::AUTH => [$_account_lid, $password]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tear down:
|
||||
* - delete users created by createUser() incl. their ACL and data
|
||||
*
|
||||
* @ToDo: implement eg. with admin_cmd_delete_user to also delete ACL and data
|
||||
*/
|
||||
public static function tearDownAfterClass() : void
|
||||
{
|
||||
$setup = self::getSetup();
|
||||
|
||||
foreach(self::$created_users as $account_lid => $data)
|
||||
{
|
||||
// if ($id) $setup->accounts->delete($data['id']);
|
||||
unset(self::$created_users[$account_lid]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add ACL rights
|
||||
*
|
||||
* @param string|array $apps app-names
|
||||
* @param string $location eg. "run"
|
||||
* @param int|string $account accountid or account_lid
|
||||
* @param int $rights rights to set, default 1
|
||||
*/
|
||||
protected static function addAcl($apps, $location, $account, $rights=1)
|
||||
{
|
||||
return self::getSetup()->add_acl($apps, $location, $account, $rights);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return instance of setup object eg. to create users
|
||||
*
|
||||
* @return \setup
|
||||
*/
|
||||
private static function getSetup()
|
||||
{
|
||||
static $setup=null;
|
||||
if (!isset($setup))
|
||||
{
|
||||
if (!isset($_REQUEST['domain']))
|
||||
{
|
||||
$_REQUEST['domain'] = $GLOBALS['EGW_DOMAIN'] ?? 'default';
|
||||
}
|
||||
$_REQUEST['ConfigDomain'] = $_REQUEST['domain'];
|
||||
|
||||
$GLOBALS['egw_info'] = array(
|
||||
'flags' => array(
|
||||
'noheader' => True,
|
||||
'nonavbar' => True,
|
||||
'currentapp' => 'setup',
|
||||
'noapi' => True
|
||||
));
|
||||
if (file_exists(__DIR__ . '/../../header.inc.php'))
|
||||
{
|
||||
include_once(__DIR__ . '/../../header.inc.php');
|
||||
}
|
||||
$setup = new \setup();
|
||||
}
|
||||
return $setup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check HTTP status in response
|
||||
*
|
||||
* @param int|array $expected one or more valid status codes
|
||||
* @param ResponseInterface $response
|
||||
* @param string $message ='' additional message to prefix result message
|
||||
*/
|
||||
protected function assertHttpStatus($expected, ResponseInterface $response, $message='')
|
||||
{
|
||||
$status = $response->getStatusCode();
|
||||
$this->assertEquals(in_array($status, (array)$expected) ? $status : ((array)$expected)[0], $status,
|
||||
(!empty($message) ? $message.': ' : ''). 'Expected HTTP status: '.json_encode($expected).
|
||||
", Server returned: $status ".$response->getReasonPhrase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts an iCal file matches an expected one taking into account $_overwrites
|
||||
*
|
||||
* @param string $_expected
|
||||
* @param string $_acctual
|
||||
* @param string $_message
|
||||
* @param array $_overwrites =[] eg. ['vEvent' => [['ATTENDEE' => ['mailto:boss@example.org' => ['PARTSTAT' => 'DECLINED']]]]]
|
||||
* (first vEvent attendee with value 'mailto:boss@...' has param 'PARTSTAT=DECLINED')
|
||||
* @throws Horde_Icalendar_Exception
|
||||
*/
|
||||
protected function assertIcal($_expected, $_acctual, $_message=null, $_overwrites=[])
|
||||
{
|
||||
// enable to see full iCals
|
||||
//$this->assertEquals($_expected, (string)$_acctual, $_message.": iCal not byte-by-byte identical");
|
||||
|
||||
$expected = new Horde_Icalendar();
|
||||
$expected->parsevCalendar($_expected);
|
||||
$acctual = new Horde_Icalendar();
|
||||
$acctual->parsevCalendar($_acctual);
|
||||
|
||||
if (($msgs = $this->checkComponentEqual($expected, $acctual, $_overwrites)))
|
||||
{
|
||||
$this->assertEquals($_expected, (string)$_acctual, ($_message ? $_message.":\n" : '').implode("\n", $msgs));
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->assertTrue(true); // due to $_overwrite probable $_expected !== $_acctual
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check two iCal components are equal modulo overwrites / expected difference
|
||||
*
|
||||
* Only a whitelist of attributes per component are checked, see $component_attrs2check variable.
|
||||
*
|
||||
* @param Horde_Icalendar $_expected
|
||||
* @param Horde_Icalendar $_acctual
|
||||
* @param string $_message
|
||||
* @param array $_overwrites =[] eg. ['ATTENDEE' => ['boss@example.org' => ['PARTSTAT' => 'DECLINED']]]
|
||||
* @throws Horde_Icalendar_Exception
|
||||
* @return array message(s) what's not equal
|
||||
*/
|
||||
protected function checkComponentEqual(Horde_Icalendar $_expected, Horde_Icalendar $_acctual, $_overwrites=[])
|
||||
{
|
||||
// only following attributes in these components are checked:
|
||||
static $component_attrs2check = [
|
||||
'vcalendar' => ['VERSION'],
|
||||
'vTimeZone' => ['TZID'],
|
||||
'vEvent' => ['UID', 'SUMMARY', 'LOCATION', 'DESCRIPTION', 'DTSTART', 'DTEND', 'ORGANIZER', 'ATTENDEE'],
|
||||
];
|
||||
|
||||
if ($_expected->getType() !== $_acctual->getType())
|
||||
{
|
||||
return ["component type not equal"];
|
||||
}
|
||||
$msgs = [];
|
||||
foreach ($component_attrs2check[$_expected->getType()] ?? [] as $attr)
|
||||
{
|
||||
$acctualAttrs = $_acctual->getAllAttributes($attr);
|
||||
foreach($_expected->getAllAttributes($attr) as $expectedAttr)
|
||||
{
|
||||
$found = false;
|
||||
foreach($acctualAttrs as $acctualAttr)
|
||||
{
|
||||
if (count($acctualAttrs) === 1 || $expectedAttr['value'] === $acctualAttr['value'])
|
||||
{
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$found)
|
||||
{
|
||||
$msgs[] = "No $attr {$expectedAttr['value']} found";
|
||||
continue;
|
||||
}
|
||||
// remove / ignore X-parameters, eg. X-EGROUPWARE-UID in ATTENDEE or ORGANIZER
|
||||
$acctualAttr['params'] = array_filter($acctualAttr['params'], function ($key) {
|
||||
return substr($key, 0, 2) !== 'X-';
|
||||
}, ARRAY_FILTER_USE_KEY);
|
||||
|
||||
if (isset($_overwrites[$attr]) && is_scalar($_overwrites[$attr]))
|
||||
{
|
||||
$expectedAttr = [
|
||||
'name' => $attr,
|
||||
'value' => $_overwrites[$attr],
|
||||
'values' => [$_overwrites[$attr]],
|
||||
'params' => [],
|
||||
];
|
||||
}
|
||||
elseif (isset($_overwrites[$attr]) && is_array($_overwrites[$attr]))
|
||||
{
|
||||
foreach ($_overwrites[$attr] as $value => $params)
|
||||
{
|
||||
if ($value === $expectedAttr['value'])
|
||||
{
|
||||
$expectedAttr['params'] = array_merge($expectedAttr['params'], $params);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($expectedAttr != $acctualAttr)
|
||||
{
|
||||
$this->assertEquals($expectedAttr, $acctualAttr, "$attr not equal");
|
||||
$msgs[] = "$attr not equal";
|
||||
}
|
||||
}
|
||||
}
|
||||
// check sub-components, overrites use an index by type eg. 1. vEvent: ['vEvent'=>[[<overwrites for 1. vEvent]]]
|
||||
$idx_by_type = [];
|
||||
foreach($_expected->getComponents() as $idx => $component)
|
||||
{
|
||||
if (!isset($idx_by_type[$type = $component->getType()])) $idx_by_type[$type] = 0;
|
||||
$msgs = array_merge($msgs, $this->checkComponentEqual($component, $_acctual->getComponent($idx),
|
||||
$_overwrites[$type][$idx_by_type[$type]] ?? []));
|
||||
$idx_by_type[$type]++;
|
||||
}
|
||||
return $msgs;
|
||||
}
|
||||
}
|
@ -25,7 +25,7 @@ class DateTimeTest extends TestCase {
|
||||
/**
|
||||
* Work in server time, so tests match expectations
|
||||
*/
|
||||
public static function setUpBeforeClass()
|
||||
public static function setUpBeforeClass() : void
|
||||
{
|
||||
parent::setUpBeforeClass();
|
||||
|
||||
@ -41,7 +41,7 @@ class DateTimeTest extends TestCase {
|
||||
// Set user time to server time for consistency
|
||||
DateTime::setUserPrefs(date_default_timezone_get());
|
||||
}
|
||||
public static function tearDownAfterClass()
|
||||
public static function tearDownAfterClass() : void
|
||||
{
|
||||
// Reset
|
||||
DateTime::setUserPrefs(static::$usertime->getName());
|
||||
|
@ -59,7 +59,7 @@ class SchemaTest extends LoggedInTest {
|
||||
/**
|
||||
* Get a database connection
|
||||
*/
|
||||
public static function setUpBeforeClass()
|
||||
public static function setUpBeforeClass() : void
|
||||
{
|
||||
parent::setUpBeforeClass();
|
||||
|
||||
|
@ -26,11 +26,11 @@ class EntryTest extends \EGroupware\Api\Etemplate\WidgetBaseTest {
|
||||
|
||||
const TEST_TEMPLATE = 'api.entry_test_contact';
|
||||
|
||||
public static function setUpBeforeClass() {
|
||||
public static function setUpBeforeClass() : void {
|
||||
parent::setUpBeforeClass();
|
||||
}
|
||||
|
||||
public function tearDown()
|
||||
protected function tearDown() : void
|
||||
{
|
||||
// Delete all elements
|
||||
foreach($this->elements as $id)
|
||||
|
@ -29,7 +29,7 @@ class DateTest extends \EGroupware\Api\Etemplate\WidgetBaseTest
|
||||
/**
|
||||
* Work in server time, so tests match expectations
|
||||
*/
|
||||
public static function setUpBeforeClass()
|
||||
public static function setUpBeforeClass() : void
|
||||
{
|
||||
parent::setUpBeforeClass();
|
||||
|
||||
@ -41,7 +41,7 @@ class DateTest extends \EGroupware\Api\Etemplate\WidgetBaseTest
|
||||
date_default_timezone_set('UTC');
|
||||
DateTime::$server_timezone = new \DateTimeZone('UTC');
|
||||
}
|
||||
public static function tearDownAfterClass()
|
||||
public static function tearDownAfterClass() : void
|
||||
{
|
||||
// Reset
|
||||
DateTime::setUserPrefs(static::$usertime->getName());
|
||||
|
@ -39,7 +39,7 @@ abstract class WidgetBaseTest extends \EGroupware\Api\LoggedInTest {
|
||||
|
||||
protected $ajax_response = null;
|
||||
|
||||
public static function setUpBeforeClass()
|
||||
public static function setUpBeforeClass() : void
|
||||
{
|
||||
parent::setUpBeforeClass();
|
||||
|
||||
@ -49,12 +49,12 @@ abstract class WidgetBaseTest extends \EGroupware\Api\LoggedInTest {
|
||||
new \EGroupware\Api\Etemplate();
|
||||
}
|
||||
|
||||
public function setUp()
|
||||
protected function setUp() : void
|
||||
{
|
||||
// Mock AJAX response
|
||||
$this->ajax_response = $this->mock_ajax_response();
|
||||
}
|
||||
public function tearDown()
|
||||
protected function tearDown() : void
|
||||
{
|
||||
// Clean up AJAX response
|
||||
$this->ajax_response->initResponseArray();
|
||||
|
@ -35,7 +35,7 @@ abstract class LoggedInTest extends TestCase
|
||||
/**
|
||||
* Start session once before each test case
|
||||
*/
|
||||
public static function setUpBeforeClass()
|
||||
public static function setUpBeforeClass() : void
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -63,7 +63,7 @@ abstract class LoggedInTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
public function assertPreConditions()
|
||||
protected function assertPreConditions() : void
|
||||
{
|
||||
// Do some checks to make sure things we expect are there
|
||||
$this->assertTrue(static::sanity_check(), 'Unable to connect to Egroupware - failed sanity check');
|
||||
@ -72,7 +72,7 @@ abstract class LoggedInTest extends TestCase
|
||||
/**
|
||||
* End session when done - every test class gets its own session
|
||||
*/
|
||||
public static function tearDownAfterClass()
|
||||
public static function tearDownAfterClass() : void
|
||||
{
|
||||
if($GLOBALS['egw'])
|
||||
{
|
||||
@ -125,11 +125,6 @@ abstract class LoggedInTest extends TestCase
|
||||
'passwd_type' => 'text',
|
||||
);
|
||||
|
||||
if (ini_get('session.save_handler') == 'files' && !is_writable(ini_get('session.save_path')) && is_dir('/tmp') && is_writable('/tmp'))
|
||||
{
|
||||
ini_set('session.save_path','/tmp'); // regular users may have no rights to apache's session dir
|
||||
}
|
||||
|
||||
if(!$info)
|
||||
{
|
||||
$info = array(
|
||||
|
@ -27,7 +27,7 @@ class BaseTest extends TestCase
|
||||
*/
|
||||
private $storage;
|
||||
|
||||
public static function setUpBeforeClass()
|
||||
public static function setUpBeforeClass() : void
|
||||
{
|
||||
if (ini_get('session.save_handler') == 'files' && !is_writable(ini_get('session.save_path')) && is_dir('/tmp') && is_writable('/tmp'))
|
||||
{
|
||||
@ -49,12 +49,12 @@ class BaseTest extends TestCase
|
||||
self::$db->connect();
|
||||
}
|
||||
|
||||
protected function setUp()
|
||||
protected function setUp() : void
|
||||
{
|
||||
$this->storage = new Api\Storage\Base('test', 'egw_test', self::$db);
|
||||
}
|
||||
|
||||
protected function assertPreConditions()
|
||||
protected function assertPreConditions() : void
|
||||
{
|
||||
$tables = self::$db->table_names(true);
|
||||
$this->assertContains('egw_test', $tables, 'Could not find DB table "egw_test", make sure test app is installed');
|
||||
|
@ -35,7 +35,7 @@ class CustomfieldsTest extends LoggedInTest
|
||||
'private' => array()
|
||||
);
|
||||
|
||||
public function assertPreConditions()
|
||||
protected function assertPreConditions() : void
|
||||
{
|
||||
parent::assertPreConditions();
|
||||
$tables = $GLOBALS['egw']->db->table_names(true);
|
||||
|
@ -61,7 +61,7 @@ class SharingBase extends LoggedInTest
|
||||
'maxdepth' => 5
|
||||
);
|
||||
|
||||
public function setUp()
|
||||
protected function setUp() : void
|
||||
{
|
||||
// Check we have basic access
|
||||
if(!is_readable($GLOBALS['egw_info']['server']['files_dir']))
|
||||
@ -75,7 +75,7 @@ class SharingBase extends LoggedInTest
|
||||
|
||||
}
|
||||
|
||||
public function tearDown()
|
||||
protected function tearDown() : void
|
||||
{
|
||||
LoggedInTest::tearDownAfterClass();
|
||||
LoggedInTest::setupBeforeClass();
|
||||
@ -239,7 +239,7 @@ class SharingBase extends LoggedInTest
|
||||
break;
|
||||
case Sharing::WRITABLE:
|
||||
// Root is not writable
|
||||
if($file == '/') continue;
|
||||
if($file == '/') break;
|
||||
|
||||
$this->assertTrue(Vfs::is_writable($file), $file . ' was not writable');
|
||||
if(!Vfs::is_dir($file))
|
||||
|
@ -21,7 +21,7 @@ use PHPUnit\Framework\TestCase as TestCase;
|
||||
|
||||
class SecurityTest extends TestCase {
|
||||
|
||||
public function setUp()
|
||||
protected function setUp() : void
|
||||
{
|
||||
// _check_script_tag uses HtmLawed, which calls GLOBALS['egw']->link()
|
||||
$GLOBALS['egw'] = $this->getMockBuilder('Egw')
|
||||
@ -30,7 +30,7 @@ class SecurityTest extends TestCase {
|
||||
->getMock();
|
||||
}
|
||||
|
||||
public function tearDown()
|
||||
protected function tearDown() : void
|
||||
{
|
||||
unset($GLOBALS['egw_inset_vars']);
|
||||
|
||||
|
@ -1338,7 +1338,7 @@ class calendar_bo
|
||||
elseif ($grants[$uid] & Acl::READ)
|
||||
{
|
||||
// if we have a READ grant from a participant, we dont give an implicit privat grant too
|
||||
$grant |= Acl::READ;
|
||||
$grant |= self::ACL_FREEBUSY | Acl::READ;
|
||||
// we cant break here, as we might be a participant too, and would miss the privat grant
|
||||
}
|
||||
elseif (!is_numeric($uid))
|
||||
|
@ -1375,35 +1375,56 @@ class calendar_groupdav extends Api\CalDAV\Handler
|
||||
return true; // simply ignore DELETE in inbox for now
|
||||
}
|
||||
$return_no_access = true; // to allow to check if current use is a participant and reject the event for him
|
||||
if (!is_array($event = $this->_common_get_put_delete('DELETE',$options,$id,$return_no_access)) || !$return_no_access ||
|
||||
// Work around problems with Outlook CalDAV Synchroniser (https://caldavsynchronizer.org/)
|
||||
// - sends a DELETE to reject a meeting request --> deletes event for all participants, if user has delete rights on the calendar
|
||||
// --> only set status for everyone else but the organizer
|
||||
self::get_agent() == 'caldavsynchronizer' && is_array($event) && $event['owner'] != $user)
|
||||
$event = $this->_common_get_put_delete('DELETE',$options,$id,$return_no_access);
|
||||
|
||||
// no event found --> 404 Not Found
|
||||
if (!is_array($event))
|
||||
{
|
||||
if (is_array($event) && (!$return_no_access || $event['owner'] != $user))
|
||||
$ret = $event;
|
||||
error_log("_common_get_put_delete('DELETE', ..., $id) user=$user, return_no_access=".array2string($return_no_access)." returned ".array2string($event));
|
||||
}
|
||||
// Work around problems with Outlook CalDAV Synchronizer (https://caldavsynchronizer.org/)
|
||||
// - sends a DELETE to reject a meeting request --> deletes event for all participants, if user has delete rights from the organizer
|
||||
// --> only set status for everyone else but the organizer
|
||||
// OR no delete rights and deleting an event in someone else calendar --> check if calendar owner is a participant --> reject him
|
||||
elseif ((!$return_no_access || (self::get_agent() === 'caldavsynchronizer' && $event['owner'] != $user)) &&
|
||||
// check if current user has edit rights for calendar of $user, can change status / reject invitation for him
|
||||
$this->bo->check_perms(Acl::EDIT, 0, $user))
|
||||
{
|
||||
// check if user is a participant or one of the groups he is a member of --> reject the meeting request
|
||||
$ret = '403 Forbidden';
|
||||
$memberships = $GLOBALS['egw']->accounts->memberships($user, true);
|
||||
foreach(array_keys($event['participants']) as $uid)
|
||||
{
|
||||
// check if user is a participant or one of the groups he is a member of --> reject the meeting request
|
||||
$ret = '403 Forbidden';
|
||||
$memberships = $GLOBALS['egw']->accounts->memberships($this->bo->user, true);
|
||||
foreach(array_keys($event['participants']) as $uid)
|
||||
if ($user == $uid || in_array($uid, $memberships))
|
||||
{
|
||||
if ($this->bo->user == $uid || in_array($uid, $memberships))
|
||||
{
|
||||
$this->bo->set_status($event,$this->bo->user, 'R');
|
||||
$ret = true;
|
||||
break;
|
||||
}
|
||||
$this->bo->set_status($event, $user, 'R');
|
||||
$ret = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
}
|
||||
// current user has no delete rights for event --> reject invitation, if he is a participant
|
||||
elseif (!$return_no_access)
|
||||
{
|
||||
// check if current user is a participant or one of the groups he is a member of --> reject the meeting request
|
||||
$ret = '403 Forbidden';
|
||||
$memberships = $GLOBALS['egw']->accounts->memberships($this->bo->user, true);
|
||||
foreach(array_keys($event['participants']) as $uid)
|
||||
{
|
||||
$ret = $event;
|
||||
if ($this->bo->user == $uid || in_array($uid, $memberships))
|
||||
{
|
||||
$this->bo->set_status($event, $this->bo->user, 'R');
|
||||
$ret = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// we have delete rights on the event and (try to) delete it
|
||||
else
|
||||
{
|
||||
$ret = $this->bo->delete($event['id']);
|
||||
if (!$ret) { error_log("delete($event[id]) returned FALSE"); $ret = '400 Failed to delete event';}
|
||||
}
|
||||
if ($this->debug) error_log(__METHOD__."(,$id) return_no_access=$return_no_access, event[participants]=".array2string(is_array($event)?$event['participants']:null).", user={$this->bo->user} --> return ".array2string($ret));
|
||||
return $ret;
|
||||
|
@ -448,7 +448,7 @@ class calendar_ical extends calendar_boupdate
|
||||
$quantity = $role = null;
|
||||
calendar_so::split_status($status, $quantity, $role);
|
||||
// do not include event owner/ORGANIZER as participant in his own calendar, if he is only participant
|
||||
if (count($event['participants']) == 1 && $event['owner'] == $uid) continue;
|
||||
if (count($event['participants']) == 1 && $event['owner'] == $uid && $uid == $this->user) continue;
|
||||
|
||||
if (!($info = $this->resource_info($uid))) continue;
|
||||
|
||||
@ -497,15 +497,15 @@ class calendar_ical extends calendar_boupdate
|
||||
{
|
||||
$user = $this->resource_info($this->user);
|
||||
$attributes['ATTENDEE'][] = 'mailto:' . $user['email'];
|
||||
$parameters['ATTENDEE'][] = array(
|
||||
'CN' => $user['name'],
|
||||
'ROLE' => 'REQ-PARTICIPANT',
|
||||
$parameters['ATTENDEE'][] = array(
|
||||
'CN' => $user['name'],
|
||||
'ROLE' => 'REQ-PARTICIPANT',
|
||||
'PARTSTAT' => 'NEEDS-ACTION',
|
||||
'CUTYPE' => 'INDIVIDUAL',
|
||||
'RSVP' => 'TRUE',
|
||||
'X-EGROUPWARE-UID' => (string)$this->user,
|
||||
);
|
||||
$event['participants'][$this->user] = true;
|
||||
);
|
||||
$event['participants'][$this->user] = true;
|
||||
}
|
||||
break;
|
||||
case 'r':
|
||||
@ -561,33 +561,33 @@ class calendar_ical extends calendar_boupdate
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ORGANIZER':
|
||||
if (!$organizerURL)
|
||||
{
|
||||
$organizerCN = '"' . trim($GLOBALS['egw']->accounts->id2name($event['owner'],'account_firstname')
|
||||
. ' ' . $GLOBALS['egw']->accounts->id2name($event['owner'],'account_lastname')) . '"';
|
||||
$organizerEMail = $GLOBALS['egw']->accounts->id2name($event['owner'],'account_email');
|
||||
if ($version == '1.0')
|
||||
{
|
||||
$organizerURL = trim($organizerCN . (empty($organizerURL) ? '' : ' <' . $organizerURL .'>'));
|
||||
}
|
||||
else
|
||||
{
|
||||
$organizerURL = empty($organizerEMail) ? '' : 'mailto:' . $organizerEMail;
|
||||
}
|
||||
$organizerUID = $event['owner'];
|
||||
}
|
||||
// do NOT use ORGANIZER for events without further participants or a different organizer
|
||||
if (count($event['participants']) > 1 || !isset($event['participants'][$event['owner']]))
|
||||
{
|
||||
$attributes['ORGANIZER'] = $organizerURL;
|
||||
$parameters['ORGANIZER']['CN'] = $organizerCN;
|
||||
if (!empty($organizerUID))
|
||||
{
|
||||
$parameters['ORGANIZER']['X-EGROUPWARE-UID'] = $organizerUID;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'ORGANIZER':
|
||||
if (!$organizerURL)
|
||||
{
|
||||
$organizerCN = '"' . trim($GLOBALS['egw']->accounts->id2name($event['owner'],'account_firstname')
|
||||
. ' ' . $GLOBALS['egw']->accounts->id2name($event['owner'],'account_lastname')) . '"';
|
||||
$organizerEMail = $GLOBALS['egw']->accounts->id2name($event['owner'],'account_email');
|
||||
if ($version == '1.0')
|
||||
{
|
||||
$organizerURL = trim($organizerCN . (empty($organizerURL) ? '' : ' <' . $organizerURL .'>'));
|
||||
}
|
||||
else
|
||||
{
|
||||
$organizerURL = empty($organizerEMail) ? '' : 'mailto:' . $organizerEMail;
|
||||
}
|
||||
$organizerUID = $event['owner'];
|
||||
}
|
||||
// do NOT use ORGANIZER for events without further participants or a different organizer
|
||||
if (count($event['participants']) > 1 || !isset($event['participants'][$event['owner']]) || $event['owner'] != $this->user)
|
||||
{
|
||||
$attributes['ORGANIZER'] = $organizerURL;
|
||||
$parameters['ORGANIZER']['CN'] = $organizerCN;
|
||||
if (!empty($organizerUID))
|
||||
{
|
||||
$parameters['ORGANIZER']['X-EGROUPWARE-UID'] = $organizerUID;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'DTSTART':
|
||||
if (empty($event['whole_day']))
|
||||
@ -1014,12 +1014,12 @@ class calendar_ical extends calendar_boupdate
|
||||
foreach (is_array($value) && $parameters[$key]['VALUE']!='DATE' ? $value : array($value) as $valueID => $valueData)
|
||||
{
|
||||
$valueData = Api\Translation::convert($valueData,Api\Translation::charset(),$charset);
|
||||
$paramData = (array) Api\Translation::convert(is_array($value) ?
|
||||
$parameters[$key][$valueID] : $parameters[$key],
|
||||
Api\Translation::charset(),$charset);
|
||||
$valuesData = (array) Api\Translation::convert($values[$key],
|
||||
Api\Translation::charset(),$charset);
|
||||
$content = $valueData . implode(';', $valuesData);
|
||||
$paramData = (array) Api\Translation::convert(is_array($value) ?
|
||||
$parameters[$key][$valueID] : $parameters[$key],
|
||||
Api\Translation::charset(),$charset);
|
||||
$valuesData = (array) Api\Translation::convert($values[$key],
|
||||
Api\Translation::charset(),$charset);
|
||||
$content = $valueData . implode(';', $valuesData);
|
||||
|
||||
if ($version == '1.0' && (preg_match('/[^\x20-\x7F]/', $content) ||
|
||||
($paramData['CN'] && preg_match('/[^\x20-\x7F]/', $paramData['CN']))))
|
||||
@ -2325,7 +2325,7 @@ class calendar_ical extends calendar_boupdate
|
||||
* @param string|resource $_vcalData
|
||||
* @param string $principalURL ='' Used for CalDAV imports
|
||||
* @param string $charset The encoding charset for $text. Defaults to
|
||||
* utf-8 for new format, iso-8859-1 for old format.
|
||||
* utf-8 for new format, iso-8859-1 for old format.
|
||||
* @return Iterator|array|boolean Iterator if resource given or array of events on success, false on failure
|
||||
*/
|
||||
function icaltoegw($_vcalData, $principalURL='', $charset=null)
|
||||
@ -2744,10 +2744,10 @@ class calendar_ical extends calendar_boupdate
|
||||
// work around Ligthning sending @ as %40
|
||||
$attributes['value'] = str_replace('%40', '@', $attributes['value']);
|
||||
if (isset($attributes['params']['PARTSTAT']))
|
||||
{
|
||||
$attributes['params']['STATUS'] = $attributes['params']['PARTSTAT'];
|
||||
}
|
||||
if (isset($attributes['params']['STATUS']))
|
||||
{
|
||||
$attributes['params']['STATUS'] = $attributes['params']['PARTSTAT'];
|
||||
}
|
||||
if (isset($attributes['params']['STATUS']))
|
||||
{
|
||||
$status = $this->status_ical2egw[strtoupper($attributes['params']['STATUS'])];
|
||||
if (empty($status)) $status = 'X';
|
||||
@ -3165,7 +3165,7 @@ class calendar_ical extends calendar_boupdate
|
||||
array2string($event)."\n",3,$this->logfile);
|
||||
}
|
||||
//Horde::logMessage("vevent2egw:\n" . print_r($event, true),
|
||||
// __FILE__, __LINE__, PEAR_LOG_DEBUG);
|
||||
// __FILE__, __LINE__, PEAR_LOG_DEBUG);
|
||||
return $event;
|
||||
}
|
||||
|
||||
|
@ -11,10 +11,10 @@
|
||||
*/
|
||||
|
||||
use EGroupware\Api;
|
||||
use EGroupware\Api\Link;
|
||||
use EGroupware\Api\Framework;
|
||||
use EGroupware\Api\Acl;
|
||||
use EGroupware\Api\Etemplate;
|
||||
use EGroupware\Api\Framework;
|
||||
use EGroupware\Api\Link;
|
||||
|
||||
/**
|
||||
* Class to generate the calendar listview and the search
|
||||
@ -137,7 +137,7 @@ class calendar_uilist extends calendar_ui
|
||||
'filter_no_lang' => True, // I set no_lang for filter (=dont translate the options)
|
||||
'no_filter2' => True, // I disable the 2. filter (params are the same as for filter)
|
||||
'no_cat' => True, // I disable the cat-selectbox
|
||||
'filter' => 'after',
|
||||
'filter' => 'month',
|
||||
'order' => 'cal_start',// IO name of the column to sort after (optional for the sortheaders)
|
||||
'sort' => 'ASC',// IO direction of the sort: 'ASC' or 'DESC'
|
||||
'default_cols' => '!week,weekday,cal_title,cal_description,recure,cal_location,cal_owner,cat_id,pm_id',
|
||||
@ -150,6 +150,12 @@ class calendar_uilist extends calendar_ui
|
||||
}
|
||||
$content['nm']['actions'] = $this->get_actions();
|
||||
|
||||
// Skip first load if view is not listview
|
||||
if($this->view && $this->view !== 'listview')
|
||||
{
|
||||
$content['nm']['num_rows'] = 0;
|
||||
}
|
||||
|
||||
if (isset($_GET['filter']) && in_array($_GET['filter'],array_keys($this->date_filters)))
|
||||
{
|
||||
$content['nm']['filter'] = $_GET['filter'];
|
||||
@ -353,6 +359,7 @@ class calendar_uilist extends calendar_ui
|
||||
break;
|
||||
|
||||
case 'month':
|
||||
default:
|
||||
$this->first = $this->bo->date2array($params['date'] ? $params['date'] : $this->date);
|
||||
$this->first['day'] = 1;
|
||||
unset($this->first['raw']);
|
||||
@ -368,9 +375,7 @@ class calendar_uilist extends calendar_ui
|
||||
$params['enddate'] = Api\DateTime::to($this->last, Api\DateTime::ET2);
|
||||
break;
|
||||
|
||||
// fall through to after given date
|
||||
case 'after':
|
||||
default:
|
||||
$this->date = $params['startdate'] ? Api\DateTime::to($params['startdate'],'ts') : $this->date;
|
||||
$label = lang('After %1',$this->bo->long_date($this->date));
|
||||
$search_params['start'] = $this->date;
|
||||
|
@ -391,7 +391,7 @@ var CalendarApp = /** @class */ (function (_super) {
|
||||
//set onbeforeunload with json request to send request when the window gets close by X button
|
||||
if (content.data.lock_token) {
|
||||
window.onbeforeunload = function () {
|
||||
this.egw.json('calendar.calendar_uiforms.ajax_unlock', [content.data.id, content.data.lock_token], null, true, null, null).sendRequest(true);
|
||||
this.egw.json('calendar.calendar_uiforms.ajax_unlock', [content.data.id, content.data.lock_token], null, true, "keepalive", null).sendRequest();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -292,7 +292,7 @@ class CalendarApp extends EgwApp
|
||||
{
|
||||
window.onbeforeunload = function () {
|
||||
this.egw.json('calendar.calendar_uiforms.ajax_unlock',
|
||||
[content.data.id, content.data.lock_token],null,true,null,null).sendRequest(true);
|
||||
[content.data.id, content.data.lock_token],null,true,"keepalive",null).sendRequest();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
121
calendar/tests/CalDAV/CalDAVcreateReadDelete.php
Normal file
121
calendar/tests/CalDAV/CalDAVcreateReadDelete.php
Normal file
@ -0,0 +1,121 @@
|
||||
<?php
|
||||
/**
|
||||
* CalDAV tests: create, read and delete an event
|
||||
*
|
||||
* @link https://www.egroupware.org
|
||||
* @author Ralf Becker <rb@egroupware.org>
|
||||
* @package calendar
|
||||
* @subpackage tests
|
||||
* @copyright (c) 2020 by Ralf Becker <rb@egroupware.org>
|
||||
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
||||
*/
|
||||
|
||||
namespace EGroupware\calendar;
|
||||
|
||||
require_once __DIR__.'/../../../api/tests/CalDAVTest.php';
|
||||
|
||||
use EGroupware\Api\CalDAVTest;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
|
||||
class CalDAVcreateReadDelete extends CalDAVTest
|
||||
{
|
||||
/**
|
||||
* Test accessing CalDAV without authentication
|
||||
*/
|
||||
public function testNoAuth()
|
||||
{
|
||||
$response = $this->getClient([])->get($this->url('/'));
|
||||
|
||||
$this->assertHttpStatus(401, $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test accessing CalDAV with authentication
|
||||
*/
|
||||
public function testAuth()
|
||||
{
|
||||
$response = $this->getClient()->get($this->url('/'));
|
||||
|
||||
$this->assertHttpStatus(200, $response);
|
||||
}
|
||||
|
||||
const EVENT_URL = '/demo/calendar/new-event-1233456789-new.ics';
|
||||
const EVENT_ICAL = <<<EOICAL
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Europe/Berlin
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:+0100
|
||||
TZOFFSETTO:+0200
|
||||
TZNAME:CEST
|
||||
DTSTART:19700329T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:+0200
|
||||
TZOFFSETTO:+0100
|
||||
TZNAME:CET
|
||||
DTSTART:19701025T030000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
DTSTART;TZID=Europe/Berlin:20110406T210000
|
||||
DTEND;TZID=Europe/Berlin:20110406T220000
|
||||
DTSTAMP:20110406T183747Z
|
||||
LAST-MODIFIED:20110406T183747Z
|
||||
LOCATION:Somewhere
|
||||
SUMMARY:Tonight
|
||||
UID:new-event-1233456789-new
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
EOICAL;
|
||||
|
||||
/**
|
||||
* Create an event
|
||||
*/
|
||||
public function testCreate()
|
||||
{
|
||||
$response = $this->getClient()->put($this->url(self::EVENT_URL), [
|
||||
RequestOptions::HEADERS => [
|
||||
'Content-Type' => 'text/calendar',
|
||||
'If-None-Match' => '*',
|
||||
],
|
||||
RequestOptions::BODY => self::EVENT_ICAL,
|
||||
]);
|
||||
|
||||
$this->assertHttpStatus(201, $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read created event
|
||||
*/
|
||||
public function testRead()
|
||||
{
|
||||
$response = $this->getClient()->get($this->url(self::EVENT_URL));
|
||||
|
||||
$this->assertHttpStatus(200, $response);
|
||||
$this->assertIcal(self::EVENT_ICAL, $response->getBody());
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete created event
|
||||
*/
|
||||
public function testDelete()
|
||||
{
|
||||
$response = $this->getClient()->delete($this->url(self::EVENT_URL));
|
||||
|
||||
$this->assertHttpStatus(204, $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read created event
|
||||
*/
|
||||
public function testReadDeleted()
|
||||
{
|
||||
$response = $this->getClient()->get($this->url(self::EVENT_URL));
|
||||
|
||||
$this->assertHttpStatus(404, $response);
|
||||
}
|
||||
}
|
368
calendar/tests/CalDAV/CalDAVsingleDELETE.php
Normal file
368
calendar/tests/CalDAV/CalDAVsingleDELETE.php
Normal file
@ -0,0 +1,368 @@
|
||||
<?php
|
||||
/**
|
||||
* CalDAV tests: DELETE requests for non-series by Outlook CalDAV Synchronizer and other clients
|
||||
*
|
||||
* @link https://www.egroupware.org
|
||||
* @author Ralf Becker <rb@egroupware.org>
|
||||
* @package calendar
|
||||
* @subpackage tests
|
||||
* @copyright (c) 2020 by Ralf Becker <rb@egroupware.org>
|
||||
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
||||
*/
|
||||
|
||||
namespace EGroupware\calendar;
|
||||
|
||||
require_once __DIR__.'/../../../api/tests/CalDAVTest.php';
|
||||
|
||||
use EGroupware\Api\CalDAVTest;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
use EGroupware\Api\Acl;
|
||||
|
||||
/**
|
||||
* Class CalDAVsingleDELETE
|
||||
*
|
||||
* This tests check all sorts of DELETE requests by organizer and attendees, with and without (delete) rights on the organizer.
|
||||
*
|
||||
* For CalDAV Synchronizer, which does not distingues between deleting and rejecting events, we only allow the
|
||||
* organizer to delete an event.
|
||||
*
|
||||
* @package EGroupware\calendar
|
||||
* @covers \calendar_groupdav::delete()
|
||||
* @uses \calendar_groupdav::put()
|
||||
* @uses \calendar_groupdav::get()
|
||||
*/
|
||||
class CalDAVsingleDELETE extends CalDAVTest
|
||||
{
|
||||
/**
|
||||
* Users and their ACL for the test
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $users = [
|
||||
'boss' => [],
|
||||
'secretary' => [
|
||||
'rights' => [
|
||||
'boss' => Acl::READ|Acl::ADD|Acl::EDIT|Acl::DELETE,
|
||||
]
|
||||
],
|
||||
'other' => [],
|
||||
];
|
||||
|
||||
/**
|
||||
* Create some users incl. ACL
|
||||
*/
|
||||
public static function setUpBeforeClass() : void
|
||||
{
|
||||
parent::setUpBeforeClass();
|
||||
|
||||
self::createUsersACL(self::$users, 'calendar');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check created users
|
||||
*/
|
||||
public function testPrincipals()
|
||||
{
|
||||
foreach(self::$users as $user => &$data)
|
||||
{
|
||||
$response = $this->getClient()->propfind($this->url('/principals/users/'.$user.'/'), [
|
||||
RequestOptions::HEADERS => [
|
||||
'Depth' => 0,
|
||||
],
|
||||
]);
|
||||
$this->assertHttpStatus(207, $response);
|
||||
}
|
||||
}
|
||||
|
||||
const EVENT_BOSS_ATTENDEE_ORGANIZER_URL = '/other/calendar/new-event-boss-attendee-123456789-new.ics';
|
||||
const EVENT_BOSS_ATTENDEE_URL = '/boss/calendar/new-event-boss-attendee-123456789-new.ics';
|
||||
const EVENT_BOSS_ATTENDEE_ICAL = <<<EOICAL
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Europe/Berlin
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:+0100
|
||||
TZOFFSETTO:+0200
|
||||
TZNAME:CEST
|
||||
DTSTART:19700329T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:+0200
|
||||
TZOFFSETTO:+0100
|
||||
TZNAME:CET
|
||||
DTSTART:19701025T030000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
DTSTART;TZID=Europe/Berlin:20110406T210000
|
||||
DTEND;TZID=Europe/Berlin:20110406T220000
|
||||
DTSTAMP:20110406T183747Z
|
||||
LAST-MODIFIED:20110406T183747Z
|
||||
LOCATION:Somewhere
|
||||
SUMMARY:Tonight
|
||||
ORGANIZER;CN="Other User":mailto:other@example.org
|
||||
ATTENDEE;CN="Other User";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:other@example.org
|
||||
ATTENDEE;CN="Boss User";CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:boss@example.org
|
||||
UID:new-event-boss-attendee-123456789-new
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
EOICAL;
|
||||
|
||||
/**
|
||||
* Check secretary deletes in boss's calendar event he is an attendee / invited
|
||||
*
|
||||
* @throws \Horde_Icalendar_Exception
|
||||
*/
|
||||
public function testSecretaryDeletesBossAttendee()
|
||||
{
|
||||
// create invitation by organizer
|
||||
$response = $this->getClient('other')->put($this->url(self::EVENT_BOSS_ATTENDEE_ORGANIZER_URL), [
|
||||
RequestOptions::HEADERS => [
|
||||
'Content-Type' => 'text/calendar',
|
||||
'Prefer' => 'return=representation'
|
||||
],
|
||||
RequestOptions::BODY => self::EVENT_BOSS_ATTENDEE_ICAL,
|
||||
]);
|
||||
$this->assertHttpStatus([200,201], $response);
|
||||
$this->assertIcal(self::EVENT_BOSS_ATTENDEE_ICAL, $response->getBody());
|
||||
|
||||
// secretrary deletes event in boss's calendar
|
||||
$response = $this->getClient('secretary')->delete($this->url(self::EVENT_BOSS_ATTENDEE_URL));
|
||||
$this->assertHttpStatus(204, $response, 'Secretary delete/rejects for boss');
|
||||
|
||||
// use organizer to check event still exists and boss rejected
|
||||
$response = $this->getClient('other')->get($this->url(self::EVENT_BOSS_ATTENDEE_ORGANIZER_URL));
|
||||
$this->assertHttpStatus(200, $response, 'Check event still exists after DELETE in attendee calendar');
|
||||
$this->assertIcal(self::EVENT_BOSS_ATTENDEE_ICAL, $response->getBody(),
|
||||
'Boss should have declined the invitation',
|
||||
['vEvent' => [['ATTENDEE' => ['mailto:boss@example.org' => ['PARTSTAT' => 'DECLINED', 'RSVP' => 'FALSE']]]]]
|
||||
);
|
||||
|
||||
// secretary tries to delete event in organizers calendar
|
||||
$response = $this->getClient('secretary')->delete($this->url(self::EVENT_BOSS_ATTENDEE_ORGANIZER_URL));
|
||||
$this->assertHttpStatus(403, $response, 'Secretary not allowed to delete for organizer');
|
||||
|
||||
// boss deletes/rejects event in his calendar
|
||||
$response = $this->getClient('boss')->delete($this->url(self::EVENT_BOSS_ATTENDEE_URL));
|
||||
$this->assertHttpStatus(204, $response, 'Boss deletes/rejects in his calendar');
|
||||
|
||||
// boss deletes/rejects event in organizers calendar
|
||||
$response = $this->getClient('boss')->delete($this->url(self::EVENT_BOSS_ATTENDEE_ORGANIZER_URL));
|
||||
$this->assertHttpStatus(204, $response, 'Boss deletes/rejects in organizers calendar');
|
||||
|
||||
// use organizer to delete event
|
||||
$response = $this->getClient('other')->delete($this->url(self::EVENT_BOSS_ATTENDEE_ORGANIZER_URL));
|
||||
$this->assertHttpStatus(204, $response);
|
||||
|
||||
// use organizer to check event deleted
|
||||
$response = $this->getClient('other')->get($this->url(self::EVENT_BOSS_ATTENDEE_ORGANIZER_URL));
|
||||
$this->assertHttpStatus(404, $response, "Check event deleted by organizer");
|
||||
}
|
||||
|
||||
const EVENT_BOSS_ORGANIZER_URL = '/boss/calendar/new-event-boss-organizer-123456789-new.ics';
|
||||
const EVENT_BOSS_ORGANIZER_OTHER_URL = '/other/calendar/new-event-boss-organizer-123456789-new.ics';
|
||||
const EVENT_BOSS_ORGANIZER_ICAL = <<<EOICAL
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Europe/Berlin
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:+0100
|
||||
TZOFFSETTO:+0200
|
||||
TZNAME:CEST
|
||||
DTSTART:19700329T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:+0200
|
||||
TZOFFSETTO:+0100
|
||||
TZNAME:CET
|
||||
DTSTART:19701025T030000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
DTSTART;TZID=Europe/Berlin:20110406T210000
|
||||
DTEND;TZID=Europe/Berlin:20110406T220000
|
||||
DTSTAMP:20110406T183747Z
|
||||
LAST-MODIFIED:20110406T183747Z
|
||||
LOCATION:Somewhere
|
||||
SUMMARY:Tonight
|
||||
ORGANIZER;CN="Boss User":mailto:boss@example.org
|
||||
ATTENDEE;CN="Boss User";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:boss@example.org
|
||||
ATTENDEE;CN="Other User";CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:other@example.org
|
||||
UID:new-event-boss-organizer-123456789-new
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
EOICAL;
|
||||
|
||||
/**
|
||||
* Check secretary deletes for boss, which is organizer of event
|
||||
*
|
||||
* @throws \Horde_Icalendar_Exception
|
||||
*/
|
||||
public function testSecretaryDeletesBossOrganizer()
|
||||
{
|
||||
// create invitation by boss as organizer
|
||||
$response = $this->getClient('boss')->put($this->url(self::EVENT_BOSS_ORGANIZER_URL), [
|
||||
RequestOptions::HEADERS => [
|
||||
'Content-Type' => 'text/calendar',
|
||||
'Prefer' => 'return=representation'
|
||||
],
|
||||
RequestOptions::BODY => self::EVENT_BOSS_ORGANIZER_ICAL,
|
||||
]);
|
||||
$this->assertHttpStatus([200,201], $response);
|
||||
$this->assertIcal(self::EVENT_BOSS_ORGANIZER_ICAL, $response->getBody());
|
||||
|
||||
// attendee deletes/rejects event in his calendar
|
||||
$response = $this->getClient('other')->delete($this->url(self::EVENT_BOSS_ORGANIZER_OTHER_URL));
|
||||
$this->assertHttpStatus(204, $response);
|
||||
|
||||
// secretrary deletes event in boss's calendar
|
||||
$response = $this->getClient('secretary')->delete($this->url(self::EVENT_BOSS_ORGANIZER_URL));
|
||||
$this->assertHttpStatus(204, $response, 'Secretary deletes for boss');
|
||||
|
||||
// use organizer/boss to check event deleted
|
||||
$response = $this->getClient('boss')->get($this->url(self::EVENT_BOSS_ORGANIZER_URL));
|
||||
$this->assertHttpStatus(404, $response, "Check event deleted by secretary");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check organizer (boss) can delete event in his calendar
|
||||
*
|
||||
* @throws \Horde_Icalendar_Exception
|
||||
*/
|
||||
public function testOrganizerDeletes()
|
||||
{
|
||||
// create invitation by boss as organizer
|
||||
$response = $this->getClient('boss')->put($this->url(self::EVENT_BOSS_ORGANIZER_URL), [
|
||||
RequestOptions::HEADERS => [
|
||||
'Content-Type' => 'text/calendar',
|
||||
'Prefer' => 'return=representation'
|
||||
],
|
||||
RequestOptions::BODY => self::EVENT_BOSS_ORGANIZER_ICAL,
|
||||
]);
|
||||
$this->assertHttpStatus([200,201], $response);
|
||||
$this->assertIcal(self::EVENT_BOSS_ORGANIZER_ICAL, $response->getBody());
|
||||
|
||||
// organizer deletes event in his calendar
|
||||
$response = $this->getClient('boss')->delete($this->url(self::EVENT_BOSS_ORGANIZER_URL));
|
||||
$this->assertHttpStatus(204, $response, 'Organizer deletes');
|
||||
|
||||
// use organizer/boss to check event deleted
|
||||
$response = $this->getClient('boss')->get($this->url(self::EVENT_BOSS_ORGANIZER_URL));
|
||||
$this->assertHttpStatus(404, $response, "Check event deleted by organizer");
|
||||
|
||||
// use attendee to check event deleted
|
||||
$response = $this->getClient('other')->get($this->url(self::EVENT_BOSS_ORGANIZER_OTHER_URL));
|
||||
$this->assertHttpStatus(404, $response, "Check event deleted by organizer");
|
||||
}
|
||||
|
||||
const EVENT_SECRETARY_ATTENDEE_URL = '/secretary/calendar/new-event-secreatary-attendee-123456789-new.ics';
|
||||
const EVENT_SECRETARY_ATTENDEE_ORGANIZER_URL = '/boss/calendar/new-event-secreatary-attendee-123456789-new.ics';
|
||||
const EVENT_SECRETARY_ATTENDEE_ICAL = <<<EOICAL
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Europe/Berlin
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:+0100
|
||||
TZOFFSETTO:+0200
|
||||
TZNAME:CEST
|
||||
DTSTART:19700329T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:+0200
|
||||
TZOFFSETTO:+0100
|
||||
TZNAME:CET
|
||||
DTSTART:19701025T030000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
DTSTART;TZID=Europe/Berlin:20110406T210000
|
||||
DTEND;TZID=Europe/Berlin:20110406T220000
|
||||
DTSTAMP:20110406T183747Z
|
||||
LAST-MODIFIED:20110406T183747Z
|
||||
LOCATION:Somewhere
|
||||
SUMMARY:Tonight
|
||||
ORGANIZER;CN="Boss User":mailto:boss@example.org
|
||||
ATTENDEE;CN="Boss User";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:boss@example.org
|
||||
ATTENDEE;CN="Secretary User";CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:secretary@example.org
|
||||
UID:new-event-secreatary-attendee-123456789-new
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
EOICAL;
|
||||
|
||||
/**
|
||||
* Check secretary as attendee deletes event
|
||||
*
|
||||
* @throws \Horde_Icalendar_Exception
|
||||
*/
|
||||
public function testSecretaryAttendeeDeletes()
|
||||
{
|
||||
// create invitation by boss as organizer
|
||||
$response = $this->getClient('boss')->put($this->url(self::EVENT_SECRETARY_ATTENDEE_ORGANIZER_URL), [
|
||||
RequestOptions::HEADERS => [
|
||||
'Content-Type' => 'text/calendar',
|
||||
'Prefer' => 'return=representation'
|
||||
],
|
||||
RequestOptions::BODY => self::EVENT_SECRETARY_ATTENDEE_ICAL,
|
||||
]);
|
||||
$this->assertHttpStatus([200,201], $response);
|
||||
$this->assertIcal(self::EVENT_SECRETARY_ATTENDEE_ICAL, $response->getBody());
|
||||
|
||||
// secretary deletes in her calendar
|
||||
$response = $this->getClient('secretary')->delete($this->url(self::EVENT_SECRETARY_ATTENDEE_URL));
|
||||
$this->assertHttpStatus(204, $response, 'Secretary (attendee) deletes');
|
||||
|
||||
// use organizer to check it's really deleted
|
||||
$response = $this->getClient('boss')->get($this->url(self::EVENT_SECRETARY_ATTENDEE_ORGANIZER_URL));
|
||||
$this->assertHttpStatus(404, $response, "Check event deleted by secretary");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check secretary as attendee deletes event with CalDAVSynchronizer
|
||||
*
|
||||
* @throws \Horde_Icalendar_Exception
|
||||
*/
|
||||
public function testSecretaryAttendeeDeletesCalDAVSynchronizer()
|
||||
{
|
||||
// create invitation by boss as organizer
|
||||
$response = $this->getClient('boss')->put($this->url(self::EVENT_SECRETARY_ATTENDEE_ORGANIZER_URL), [
|
||||
RequestOptions::HEADERS => [
|
||||
'Content-Type' => 'text/calendar',
|
||||
'Prefer' => 'return=representation'
|
||||
],
|
||||
RequestOptions::BODY => self::EVENT_SECRETARY_ATTENDEE_ICAL,
|
||||
]);
|
||||
$this->assertHttpStatus([200,201], $response);
|
||||
$this->assertIcal(self::EVENT_SECRETARY_ATTENDEE_ICAL, $response->getBody());
|
||||
|
||||
// secretary deletes in her calendar with CalDAVSynchronizer
|
||||
$response = $this->getClient('secretary')->delete($this->url(self::EVENT_SECRETARY_ATTENDEE_URL),
|
||||
[RequestOptions::HEADERS => ['User-Agent' => 'CalDAVSynchronizer']]);
|
||||
$this->assertHttpStatus(204, $response, 'Secretary (attendee) deletes/rejects');
|
||||
|
||||
// use organizer to check it's NOT deleted, as CalDAVSynchronizer / Outlook does not distinguish between reject and delete
|
||||
$response = $this->getClient('boss')->get($this->url(self::EVENT_SECRETARY_ATTENDEE_ORGANIZER_URL));
|
||||
$this->assertHttpStatus(200, $response, "Check event NOT deleted by secretary");
|
||||
$this->assertIcal(self::EVENT_SECRETARY_ATTENDEE_ICAL, $response->getBody(),
|
||||
'Secretary should have declined the invitation',
|
||||
['vEvent' => [['ATTENDEE' => ['mailto:secretary@example.org' => ['PARTSTAT' => 'DECLINED', 'RSVP' => 'FALSE']]]]]
|
||||
);
|
||||
|
||||
// organizer deletes in his calendar with CalDAVSynchronizer
|
||||
$response = $this->getClient('boss')->delete($this->url(self::EVENT_SECRETARY_ATTENDEE_ORGANIZER_URL),
|
||||
[RequestOptions::HEADERS => ['User-Agent' => 'CalDAVSynchronizer']]);
|
||||
$this->assertHttpStatus(204, $response, 'Organizer deletes');
|
||||
|
||||
// use organizer to check it's deleted, as CalDAVSynchronizer / Outlook should still delete for organizer
|
||||
$response = $this->getClient('boss')->get($this->url(self::EVENT_SECRETARY_ATTENDEE_ORGANIZER_URL));
|
||||
$this->assertHttpStatus(404, $response, "Check event deleted by organizer");
|
||||
}
|
||||
}
|
@ -25,17 +25,17 @@ class ImportParticipantsTest extends \EGroupware\Api\AppTest
|
||||
// Method under test with modified access
|
||||
private $parse_method = null;
|
||||
|
||||
public static function setUpBeforeClass()
|
||||
public static function setUpBeforeClass() : void
|
||||
{
|
||||
parent::setUpBeforeClass();
|
||||
|
||||
}
|
||||
public static function tearDownAfterClass()
|
||||
public static function tearDownAfterClass() : void
|
||||
{
|
||||
parent::tearDownAfterClass();
|
||||
}
|
||||
|
||||
public function setUp()
|
||||
protected function setUp() : void
|
||||
{
|
||||
$this->bo = new \calendar_bo();
|
||||
|
||||
@ -50,7 +50,7 @@ class ImportParticipantsTest extends \EGroupware\Api\AppTest
|
||||
$this->parse_method->setAccessible(true);
|
||||
}
|
||||
|
||||
public function tearDown()
|
||||
protected function tearDown() : void
|
||||
{
|
||||
|
||||
}
|
||||
|
@ -24,17 +24,17 @@ class ResetParticipantStatusTest extends \EGroupware\Api\AppTest
|
||||
// Method under test with modified access
|
||||
private $check_method = null;
|
||||
|
||||
public static function setUpBeforeClass()
|
||||
public static function setUpBeforeClass() : void
|
||||
{
|
||||
parent::setUpBeforeClass();
|
||||
|
||||
}
|
||||
public static function tearDownAfterClass()
|
||||
public static function tearDownAfterClass() : void
|
||||
{
|
||||
parent::tearDownAfterClass();
|
||||
}
|
||||
|
||||
public function setUp()
|
||||
protected function setUp() : void
|
||||
{
|
||||
$this->bo = new \calendar_boupdate();
|
||||
|
||||
@ -47,7 +47,7 @@ class ResetParticipantStatusTest extends \EGroupware\Api\AppTest
|
||||
$this->check_method->setAccessible(true);
|
||||
}
|
||||
|
||||
public function tearDown()
|
||||
protected function tearDown() : void
|
||||
{
|
||||
|
||||
// Clean up user
|
||||
|
@ -28,20 +28,20 @@ class TimezoneTest extends \EGroupware\Api\AppTest {
|
||||
protected $recur_end;
|
||||
protected $cal_id;
|
||||
|
||||
public static function setUpBeforeClass()
|
||||
public static function setUpBeforeClass() : void
|
||||
{
|
||||
parent::setUpBeforeClass();
|
||||
|
||||
static::$server_tz = date_default_timezone_get();
|
||||
}
|
||||
public static function tearDownAfterClass()
|
||||
public static function tearDownAfterClass() : void
|
||||
{
|
||||
date_default_timezone_set(static::$server_tz);
|
||||
|
||||
parent::tearDownAfterClass();
|
||||
}
|
||||
|
||||
public function setUp()
|
||||
protected function setUp() : void
|
||||
{
|
||||
$this->bo = new \calendar_boupdate();
|
||||
|
||||
@ -50,7 +50,7 @@ class TimezoneTest extends \EGroupware\Api\AppTest {
|
||||
$this->recur_end = new Api\DateTime(mktime(0,0,0,date('m'), date('d') + static::RECUR_DAYS, date('Y')));
|
||||
}
|
||||
|
||||
public function tearDown()
|
||||
protected function tearDown() : void
|
||||
{
|
||||
$this->bo->delete($this->cal_id);
|
||||
// Delete again to remove from delete history
|
||||
|
@ -45,12 +45,12 @@
|
||||
],
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "7.0"
|
||||
"php": "7.2"
|
||||
},
|
||||
"sort-packages": true
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.0,<=8.0.0alpha1",
|
||||
"php": ">=7.2,<=8.0.0alpha1",
|
||||
"ext-gd": "*",
|
||||
"ext-json": "*",
|
||||
"ext-mysqli": "*",
|
||||
@ -101,8 +101,10 @@
|
||||
"tinymce/tinymce": "^5.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"guzzlehttp/guzzle": "^6.5",
|
||||
"phpunit/phpunit": "~8"
|
||||
},
|
||||
"suggests": {
|
||||
"suggest": {
|
||||
"ext-opcache": "Opcode cache to speed up PHP",
|
||||
"ext-apcu": "Used for in-memory caching",
|
||||
"ext-tidy": "Used for tidying up docx templates",
|
||||
|
1854
composer.lock
generated
1854
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -14,6 +14,9 @@ The provided docker-compose.yml will run the following container:
|
||||
* **egroupware-nginx** running Nginx as webserver (by default http only on port 8080)
|
||||
* **egroupware-db** latest MariaDB 10.4
|
||||
* **egroupware-watchtower** updating all above container automatically daily at 4am
|
||||
* **collabora-key** Collabora Online Office
|
||||
* **rocketchat** Rocket.Chat server
|
||||
* **rocketchat-mongodb** MongoDB for Rocket.Chat
|
||||
```
|
||||
version: '3'
|
||||
volumes:
|
||||
@ -36,6 +39,29 @@ volumes:
|
||||
# # location of deprecated EGroupware packages like Wiki, SiteMgr, KnowledgeBase
|
||||
# device: /usr/share/egroupware
|
||||
# #device: $PWD/extra
|
||||
# collabora-config
|
||||
collabora-config:
|
||||
driver_opts:
|
||||
type: none
|
||||
o: bind
|
||||
# to upgrade an existing non-docker installation most easy is to use the existing
|
||||
# data directory /var/lib/egroupware AND the host database see below
|
||||
#device: /var/lib/egroupware/default/loolwsd
|
||||
# otherwise data is stored in data subdirectory of the current directory
|
||||
device: $PWD/data/default/loolwsd
|
||||
# store Rocket.Chat MongoDB on an (internal) Volume
|
||||
mongo:
|
||||
# directory to store MongoDB dumps
|
||||
rocketchat-dumps:
|
||||
driver_opts:
|
||||
type: none
|
||||
o: bind
|
||||
device: $PWD/data/default/rocketchat/dump
|
||||
rocketchat-uploads:
|
||||
driver_opts:
|
||||
type: none
|
||||
o: bind
|
||||
device: $PWD/data/default/rocketchat/uploads
|
||||
services:
|
||||
egroupware:
|
||||
image: egroupware/egroupware:latest
|
||||
@ -52,13 +78,18 @@ services:
|
||||
# 1. comment out the whole db service below AND
|
||||
# 2. set EGW_DB_HOST=localhost AND
|
||||
# 3. uncomment the next line and modify the host path (first one), it depends on your distro:
|
||||
# - RHEL/CentOS /var/lib/mysql/mysql.sock
|
||||
# - openSUSE/SLE /var/run/mysql/mysql.sock
|
||||
# - Debian/Ubuntu /var/run/mysqld/mysqld.sock
|
||||
#- /var/run/mysqld/mysqld.sock:/var/run/mysqld/mysqld.sock
|
||||
# - RHEL/CentOS /var/lib/mysql/mysql.sock:/var/run/mysqld/mysqld.sock
|
||||
# - openSUSE/SLE /var/run/mysql/mysql.sock:/var/run/mysqld/mysqld.sock
|
||||
# - Debian/Ubuntu /var/run/mysqld:/var/run/mysqld
|
||||
#- /var/run/mysqld:/var/run/mysqld
|
||||
# private CA so egroupware can validate your certificate to talk to Collabora or Rocket.Chat
|
||||
# multiple certificates (eg. a chain) have to be single files in a directory, with one named private-ca.crt!
|
||||
#- /etc/egroupware-docker/private-ca.crt:/usr/local/share/ca-certificates/private-ca.crt:ro
|
||||
environment:
|
||||
# MariaDB/MySQL host to use: for internal service use "db", for host database (socket bind-mounted into container) use "localhost"
|
||||
- EGW_DB_HOST=db
|
||||
# grant host is needed for NOT using localhost / unix domain socket for MySQL/MariaDB
|
||||
- EGW_DB_GRANT_HOST=172.%
|
||||
# for internal db service you should to specify a root password here AND in db service
|
||||
# a database "egroupware" with a random password is created for you on installation (password is stored in header.inc.php in data directory)
|
||||
#- EGW_DB_ROOT=root
|
||||
@ -133,4 +164,66 @@ services:
|
||||
command: --schedule "0 0 4 * * *"
|
||||
container_name: egroupware-watchtower
|
||||
restart: always
|
||||
|
||||
# Collabora Online Office
|
||||
collabora-key:
|
||||
image: "quay.io/egroupware/collabora-key:stable"
|
||||
#image: collabora/code:latest
|
||||
# needs to be initialised via: docker run --rm -v dev_collabora-config:/mnt --entrypoint '/bin/cp -r /etc/loolwsd /mnt' quay.io/egroupware/collabora-key:stable
|
||||
volumes:
|
||||
- collabora-config:/etc/loolwsd
|
||||
# dont try to regenerate the (not used certificate) as volumn is readonly
|
||||
environment:
|
||||
- DONT_GEN_SSL_CERT=1
|
||||
restart: always
|
||||
container_name: collabora-key
|
||||
# set the ip-address of your docker host AND your official DNS name so Collabora
|
||||
# can access EGroupware without the need to go over your firewall
|
||||
#extra_hosts:
|
||||
#- "my.host.name:ip-address"
|
||||
|
||||
# Rocket.Chat server
|
||||
rocketchat:
|
||||
image: rocketchat/rocket.chat:latest
|
||||
command: bash -c 'for i in `seq 1 30`; do node main.js && s=$$? && break || s=$$?; echo "Tried $$i times. Waiting 5 secs..."; sleep 5; done; (exit $$s)'
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- rocketchat-uploads:/app/uploads
|
||||
# if EGroupware uses a certificate from a private CA, OAuth authentication will fail, you need to:
|
||||
# - have the CA certificate stored at /etc/egroupware-docker/private-ca.crt
|
||||
# - uncomment the next 2 lines about the private CA:
|
||||
# - /etc/egroupware-docker/private-ca.crt:/usr/local/share/ca-certificates/private-ca.crt:ro
|
||||
environment:
|
||||
# - NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/private-ca.crt
|
||||
# IMPORTANT: change ROOT_URL to your actual url eg. https://domain.com/rocketchat
|
||||
- ROOT_URL=http://localhost/rocketchat
|
||||
- PORT=3000
|
||||
- MONGO_URL=mongodb://mongo:27017/rocketchat
|
||||
- MONGO_OPLOG_URL=mongodb://mongo:27017/local
|
||||
# - HTTP_PROXY=http://proxy.domain.com
|
||||
# - HTTPS_PROXY=http://proxy.domain.com
|
||||
depends_on:
|
||||
- mongo
|
||||
container_name: rocketchat
|
||||
# set the ip-address of your docker host AND your official DNS name so Rocket.Chat
|
||||
# can access EGroupware without the need to go over your firewall
|
||||
#extra_hosts:
|
||||
#- "my.host.name:ip-address"
|
||||
|
||||
# MongoDB for Rocket.Chat
|
||||
mongo:
|
||||
image: mongo:4.0
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- mongo:/data/db
|
||||
- rocketchat-dumps:/dump
|
||||
command: mongod --smallfiles --oplogSize 128 --replSet rs0 --storageEngine=mmapv1
|
||||
container_name: rocketchat-mongo
|
||||
# this container's job is just run the command to initialize the replica set.
|
||||
# it will run the command and remove himself (it will not stay running)
|
||||
mongo-init-replica:
|
||||
image: mongo:4.0
|
||||
command: 'bash -c "for i in `seq 1 30`; do mongo mongo/rocketchat --eval \"rs.initiate({ _id: ''rs0'', members: [ { _id: 0, host: ''localhost:27017'' } ]})\" && s=$$? && break || s=$$?; echo \"Tried $$i times. Waiting 5 secs...\"; sleep 5; done; (exit $$s)"'
|
||||
depends_on:
|
||||
- mongo
|
||||
```
|
||||
|
@ -7,6 +7,10 @@ The container and docker-compose.yml file in this directory are the most easy wa
|
||||
* data: EGroupware stores it's files here, by default $PWD/data subdirectory, can also be your existing /var/lib/egroupware
|
||||
* db: volume for MariaDB (should be NOT a directory under Mac OS and Windows for performance reasons!)
|
||||
* sessions: volume for sessions, internal no need to change
|
||||
* sources-push: swoolpush sub-directory of sources
|
||||
* collabora-config: /etc/loolwsd for Collabora container, by default $PWD/data/default/loolwsd
|
||||
* rocketchat-uploads: Upload directory for Rocket.Chat, by default $PWD/data/default/rocketchat/uploads
|
||||
* rocketchat-dumps: Dump directory for MongoDB, by default $PWD/data/default/rocketchat/dump
|
||||
|
||||
### It runs the following containers:
|
||||
* egroupware: php-fpm
|
||||
@ -14,11 +18,9 @@ The container and docker-compose.yml file in this directory are the most easy wa
|
||||
* egroupware-db: MariaDB
|
||||
* egroupware-push: PHP Swoole based push server
|
||||
* egroupware-watchtower: to automatic keeps the containers up to date
|
||||
|
||||
Planned, but not yet there:
|
||||
* egroupware-collabora: Collabora Online Office
|
||||
* egroupware-rocketchat: Rocket.Chat
|
||||
* egroupware-mongo: MongoDB for Rocket.Chat
|
||||
* collabora: Collabora Online Office
|
||||
* rocketchat: Rocket.Chat
|
||||
* rocketchat-mongo: MongoDB for Rocket.Chat
|
||||
|
||||
### Usage:
|
||||
```
|
||||
|
@ -40,6 +40,20 @@ volumes:
|
||||
sessions:
|
||||
# cache files from compose, npm and yarn (actually /root inside the container)
|
||||
cache:
|
||||
# store Rocket.Chat MongoDB on an (internal) Volume
|
||||
mongo:
|
||||
# directory to store MongoDB dumps
|
||||
rocketchat-dumps:
|
||||
driver_opts:
|
||||
type: none
|
||||
o: bind
|
||||
device: $PWD/data/default/rocketchat/dump
|
||||
rocketchat-uploads:
|
||||
driver_opts:
|
||||
type: none
|
||||
o: bind
|
||||
device: $PWD/data/default/rocketchat/uploads
|
||||
|
||||
services:
|
||||
egroupware:
|
||||
# you can also use tags like: 7.3, 7.3.12 or 7.4
|
||||
@ -56,10 +70,13 @@ services:
|
||||
# 1. comment out the whole db service below AND
|
||||
# 2. set EGW_DB_HOST=localhost AND
|
||||
# 3. uncomment the next line and modify the host path (first one), it depends on your distro:
|
||||
# - RHEL/CentOS /var/lib/mysql/mysql.sock
|
||||
# - openSUSE/SLE /var/run/mysql/mysql.sock
|
||||
# - Debian/Ubuntu /var/run/mysqld/mysqld.sock
|
||||
#- /var/run/mysqld/mysqld.sock:/var/run/mysqld/mysqld.sock
|
||||
# - RHEL/CentOS /var/lib/mysql/mysql.sock:/var/run/mysqld/mysqld.sock
|
||||
# - openSUSE/SLE /var/run/mysql/mysql.sock:/var/run/mysqld/mysqld.sock
|
||||
# - Debian/Ubuntu /var/run/mysqld:/var/run/mysqld
|
||||
#- /var/run/mysqld:/var/run/mysqld
|
||||
# private CA so egroupware can validate your certificate to talk to Collabora or Rocket.Chat
|
||||
# multiple certificates (eg. a chain) have to be single files in a directory, with one named private-ca.crt!
|
||||
#- /etc/egroupware-docker/private-ca.crt:/usr/local/share/ca-certificates/private-ca.crt:ro
|
||||
environment:
|
||||
#
|
||||
# MariaDB/MySQL host to use: for internal service use "db", for host database (socket bind-mounted into container) use "localhost"
|
||||
@ -122,6 +139,7 @@ services:
|
||||
depends_on:
|
||||
- egroupware
|
||||
- collabora-key
|
||||
- rocketchat
|
||||
container_name: egroupware-nginx
|
||||
|
||||
# run an own MariaDB:10.4 (you can use EGroupware's database backup and restore to add your existing database)
|
||||
@ -185,3 +203,48 @@ services:
|
||||
# can access EGroupware without the need to go over your firewall
|
||||
#extra_hosts:
|
||||
#- "my.host.name:ip-address"
|
||||
|
||||
# Rocket.Chat server
|
||||
rocketchat:
|
||||
image: rocketchat/rocket.chat:latest
|
||||
command: bash -c 'for i in `seq 1 30`; do node main.js && s=$$? && break || s=$$?; echo "Tried $$i times. Waiting 5 secs..."; sleep 5; done; (exit $$s)'
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- rocketchat-uploads:/app/uploads
|
||||
# if EGroupware uses a certificate from a private CA, OAuth authentication will fail, you need to:
|
||||
# - have the CA certificate stored at /etc/egroupware-docker/private-ca.crt
|
||||
# - uncomment the next 2 lines about the private CA:
|
||||
# - /etc/egroupware-docker/private-ca.crt:/usr/local/share/ca-certificates/private-ca.crt:ro
|
||||
environment:
|
||||
# - NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/private-ca.crt
|
||||
# IMPORTANT: change ROOT_URL to your actual url eg. https://domain.com/rocketchat
|
||||
- ROOT_URL=http://localhost/rocketchat
|
||||
- PORT=3000
|
||||
- MONGO_URL=mongodb://mongo:27017/rocketchat
|
||||
- MONGO_OPLOG_URL=mongodb://mongo:27017/local
|
||||
# - HTTP_PROXY=http://proxy.domain.com
|
||||
# - HTTPS_PROXY=http://proxy.domain.com
|
||||
depends_on:
|
||||
- mongo
|
||||
container_name: rocketchat
|
||||
# set the ip-address of your docker host AND your official DNS name so Rocket.Chat
|
||||
# can access EGroupware without the need to go over your firewall
|
||||
#extra_hosts:
|
||||
#- "my.host.name:ip-address"
|
||||
|
||||
# MongoDB for Rocket.Chat
|
||||
mongo:
|
||||
image: mongo:4.0
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- mongo:/data/db
|
||||
- rocketchat-dumps:/dump
|
||||
command: mongod --smallfiles --oplogSize 128 --replSet rs0 --storageEngine=mmapv1
|
||||
container_name: rocketchat-mongo
|
||||
# this container's job is just run the command to initialize the replica set.
|
||||
# it will run the command and remove himself (it will not stay running)
|
||||
mongo-init-replica:
|
||||
image: mongo:4.0
|
||||
command: 'bash -c "for i in `seq 1 30`; do mongo mongo/rocketchat --eval \"rs.initiate({ _id: ''rs0'', members: [ { _id: 0, host: ''localhost:27017'' } ]})\" && s=$$? && break || s=$$?; echo \"Tried $$i times. Waiting 5 secs...\"; sleep 5; done; (exit $$s)"'
|
||||
depends_on:
|
||||
- mongo
|
||||
|
@ -42,8 +42,8 @@ server {
|
||||
fastcgi_read_timeout 60m;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_split_path_info ^((?U).+\.php)(.*)$;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
|
||||
# standard Nginx
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME /var/www/egroupware$1;
|
||||
@ -80,9 +80,9 @@ server {
|
||||
fastcgi_read_timeout 60m;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
}
|
||||
|
||||
# ActiveSync support
|
||||
@ -92,8 +92,8 @@ server {
|
||||
fastcgi_read_timeout 60m;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_split_path_info ^((?U).+\.php)(.*)$;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME /var/www/egroupware/activesync/index.php;
|
||||
}
|
||||
@ -110,25 +110,38 @@ server {
|
||||
location = / {
|
||||
return 301 $scheme://$http_host/egroupware/index.php;
|
||||
}
|
||||
# redirect /egroupware to /egroupware/
|
||||
location = /egroupware {
|
||||
return 301 $scheme://$host/egroupware/index.php;
|
||||
}
|
||||
|
||||
# Collabora sniplet meant to be included in server block of EGroupware vhost
|
||||
# static files
|
||||
location ^~ /loleaflet {
|
||||
proxy_pass http://collabora-key:9980;
|
||||
proxy_set_header Host $http_host;
|
||||
}
|
||||
# static files
|
||||
location ^~ /loleaflet {
|
||||
proxy_pass http://collabora-key:9980;
|
||||
proxy_set_header Host $http_host;
|
||||
}
|
||||
|
||||
# WOPI discovery URL
|
||||
location ^~ /hosting/discovery {
|
||||
proxy_pass http://collabora-key:9980;
|
||||
proxy_set_header Host $http_host;
|
||||
}
|
||||
# WOPI discovery URL
|
||||
location ^~ /hosting/discovery {
|
||||
proxy_pass http://collabora-key:9980;
|
||||
proxy_set_header Host $http_host;
|
||||
}
|
||||
|
||||
# websockets, download, presentation and image upload
|
||||
location ^~ /lool {
|
||||
proxy_pass http://collabora-key:9980;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $http_host;
|
||||
}
|
||||
# websockets, download, presentation and image upload
|
||||
location ^~ /lool {
|
||||
proxy_pass http://collabora-key:9980;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $http_host;
|
||||
}
|
||||
|
||||
# Rocket.Chat sniplet meant to be included in server block of EGroupware vhost
|
||||
# proxy into rocketchat container
|
||||
location /rocketchat {
|
||||
proxy_pass http://rocketchat:3000;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $http_host;
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,29 @@ volumes:
|
||||
# # location of deprecated EGroupware packages like Wiki, SiteMgr, KnowledgeBase
|
||||
# device: /usr/share/egroupware
|
||||
# #device: $PWD/extra
|
||||
# collabora-config
|
||||
collabora-config:
|
||||
driver_opts:
|
||||
type: none
|
||||
o: bind
|
||||
# to upgrade an existing non-docker installation most easy is to use the existing
|
||||
# data directory /var/lib/egroupware AND the host database see below
|
||||
#device: /var/lib/egroupware/default/loolwsd
|
||||
# otherwise data is stored in data subdirectory of the current directory
|
||||
device: $PWD/data/default/loolwsd
|
||||
# store Rocket.Chat MongoDB on an (internal) Volume
|
||||
mongo:
|
||||
# directory to store MongoDB dumps
|
||||
rocketchat-dumps:
|
||||
driver_opts:
|
||||
type: none
|
||||
o: bind
|
||||
device: $PWD/data/default/rocketchat/dump
|
||||
rocketchat-uploads:
|
||||
driver_opts:
|
||||
type: none
|
||||
o: bind
|
||||
device: $PWD/data/default/rocketchat/uploads
|
||||
services:
|
||||
egroupware:
|
||||
image: egroupware/egroupware:latest
|
||||
@ -35,10 +58,13 @@ services:
|
||||
# 1. comment out the whole db service below AND
|
||||
# 2. set EGW_DB_HOST=localhost AND
|
||||
# 3. uncomment the next line and modify the host path (first one), it depends on your distro:
|
||||
# - RHEL/CentOS /var/lib/mysql/mysql.sock
|
||||
# - openSUSE/SLE /var/run/mysql/mysql.sock
|
||||
# - Debian/Ubuntu /var/run/mysqld/mysqld.sock
|
||||
#- /var/run/mysqld/mysqld.sock:/var/run/mysqld/mysqld.sock
|
||||
# - RHEL/CentOS /var/lib/mysql/mysql.sock:/var/run/mysqld/mysqld.sock
|
||||
# - openSUSE/SLE /var/run/mysql/mysql.sock:/var/run/mysqld/mysqld.sock
|
||||
# - Debian/Ubuntu /var/run/mysqld:/var/run/mysqld
|
||||
#- /var/run/mysqld:/var/run/mysqld
|
||||
# private CA so egroupware can validate your certificate to talk to Collabora or Rocket.Chat
|
||||
# multiple certificates (eg. a chain) have to be single files in a directory, with one named private-ca.crt!
|
||||
#- /etc/egroupware-docker/private-ca.crt:/usr/local/share/ca-certificates/private-ca.crt:ro
|
||||
environment:
|
||||
# MariaDB/MySQL host to use: for internal service use "db", for host database (socket bind-mounted into container) use "localhost"
|
||||
- EGW_DB_HOST=db
|
||||
@ -83,6 +109,8 @@ services:
|
||||
- "4443:443"
|
||||
depends_on:
|
||||
- egroupware
|
||||
- collabora-key
|
||||
- rocketchat
|
||||
container_name: egroupware-nginx
|
||||
|
||||
# run an own MariaDB:10.4 (you can use EGroupware's database backup and restore to add your existing database)
|
||||
@ -118,3 +146,65 @@ services:
|
||||
command: --schedule "0 0 4 * * *"
|
||||
container_name: egroupware-watchtower
|
||||
restart: always
|
||||
|
||||
# Collabora Online Office
|
||||
collabora-key:
|
||||
image: "quay.io/egroupware/collabora-key:stable"
|
||||
#image: collabora/code:latest
|
||||
# needs to be initialised via: docker run --rm -v dev_collabora-config:/mnt --entrypoint '/bin/cp -r /etc/loolwsd /mnt' quay.io/egroupware/collabora-key:stable
|
||||
volumes:
|
||||
- collabora-config:/etc/loolwsd
|
||||
# dont try to regenerate the (not used certificate) as volumn is readonly
|
||||
environment:
|
||||
- DONT_GEN_SSL_CERT=1
|
||||
restart: always
|
||||
container_name: collabora-key
|
||||
# set the ip-address of your docker host AND your official DNS name so Collabora
|
||||
# can access EGroupware without the need to go over your firewall
|
||||
#extra_hosts:
|
||||
#- "my.host.name:ip-address"
|
||||
|
||||
# Rocket.Chat server
|
||||
rocketchat:
|
||||
image: rocketchat/rocket.chat:latest
|
||||
command: bash -c 'for i in `seq 1 30`; do node main.js && s=$$? && break || s=$$?; echo "Tried $$i times. Waiting 5 secs..."; sleep 5; done; (exit $$s)'
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- rocketchat-uploads:/app/uploads
|
||||
# if EGroupware uses a certificate from a private CA, OAuth authentication will fail, you need to:
|
||||
# - have the CA certificate stored at /etc/egroupware-docker/private-ca.crt
|
||||
# - uncomment the next 2 lines about the private CA:
|
||||
# - /etc/egroupware-docker/private-ca.crt:/usr/local/share/ca-certificates/private-ca.crt:ro
|
||||
environment:
|
||||
# - NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/private-ca.crt
|
||||
# IMPORTANT: change ROOT_URL to your actual url eg. https://domain.com/rocketchat
|
||||
- ROOT_URL=http://localhost/rocketchat
|
||||
- PORT=3000
|
||||
- MONGO_URL=mongodb://mongo:27017/rocketchat
|
||||
- MONGO_OPLOG_URL=mongodb://mongo:27017/local
|
||||
# - HTTP_PROXY=http://proxy.domain.com
|
||||
# - HTTPS_PROXY=http://proxy.domain.com
|
||||
depends_on:
|
||||
- mongo
|
||||
container_name: rocketchat
|
||||
# set the ip-address of your docker host AND your official DNS name so Rocket.Chat
|
||||
# can access EGroupware without the need to go over your firewall
|
||||
#extra_hosts:
|
||||
#- "my.host.name:ip-address"
|
||||
|
||||
# MongoDB for Rocket.Chat
|
||||
mongo:
|
||||
image: mongo:4.0
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- mongo:/data/db
|
||||
- rocketchat-dumps:/dump
|
||||
command: mongod --smallfiles --oplogSize 128 --replSet rs0 --storageEngine=mmapv1
|
||||
container_name: rocketchat-mongo
|
||||
# this container's job is just run the command to initialize the replica set.
|
||||
# it will run the command and remove himself (it will not stay running)
|
||||
mongo-init-replica:
|
||||
image: mongo:4.0
|
||||
command: 'bash -c "for i in `seq 1 30`; do mongo mongo/rocketchat --eval \"rs.initiate({ _id: ''rs0'', members: [ { _id: 0, host: ''localhost:27017'' } ]})\" && s=$$? && break || s=$$?; echo \"Tried $$i times. Waiting 5 secs...\"; sleep 5; done; (exit $$s)"'
|
||||
depends_on:
|
||||
- mongo
|
||||
|
@ -1,8 +1,16 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# if EGW_APC_SHM_SIZE is set in environment, propagate value to apcu.ini
|
||||
test -n "$EGW_APC_SHM_SIZE" && {
|
||||
grep "apc.shm_size" /etc/php/7.3/fpm/conf.d/20-apcu.ini >/dev/null && \
|
||||
sed -e "s/^;\?apc.shm_size.*/apc.shm_size=$EGW_APC_SHM_SIZE/g" \
|
||||
-i /etc/php/7.3/fpm/conf.d/20-apcu.ini || \
|
||||
echo "apc.shm_size=$EGW_APC_SHM_SIZE" >> /etc/php/7.3/fpm/conf.d/20-apcu.ini \
|
||||
}
|
||||
|
||||
# if EGW_SESSION_TIMEOUT is set in environment, propagate value to php.ini
|
||||
test -n "$EGW_SESSION_TIMEOUT" && test "$EGW_SESSION_TIMEOUT" -ge 1440 &&
|
||||
test -n "$EGW_SESSION_TIMEOUT" && test "$EGW_SESSION_TIMEOUT" -ge 1440 && \
|
||||
sed -e "s/^;\?session.gc_maxlifetime.*/session.gc_maxlifetime=$EGW_SESSION_TIMEOUT/g" \
|
||||
-i /etc/php/7.3/fpm/php.ini
|
||||
|
||||
|
@ -121,4 +121,38 @@ server {
|
||||
location = / {
|
||||
return 301 $redirectscheme://$host/egroupware/index.php;
|
||||
}
|
||||
# redirect /egroupware to /egroupware/
|
||||
location = /egroupware {
|
||||
return 301 $redirectscheme://$host/egroupware/index.php;
|
||||
}
|
||||
|
||||
# Collabora sniplet meant to be included in server block of EGroupware vhost
|
||||
# static files
|
||||
location ^~ /loleaflet {
|
||||
proxy_pass http://collabora-key:9980;
|
||||
proxy_set_header Host $http_host;
|
||||
}
|
||||
|
||||
# WOPI discovery URL
|
||||
location ^~ /hosting/discovery {
|
||||
proxy_pass http://collabora-key:9980;
|
||||
proxy_set_header Host $http_host;
|
||||
}
|
||||
|
||||
# websockets, download, presentation and image upload
|
||||
location ^~ /lool {
|
||||
proxy_pass http://collabora-key:9980;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $http_host;
|
||||
}
|
||||
|
||||
# Rocket.Chat sniplet meant to be included in server block of EGroupware vhost
|
||||
# proxy into rocketchat container
|
||||
location /rocketchat {
|
||||
proxy_pass http://rocketchat:3000;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $http_host;
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,17 @@ if (!class_exists('\PHPUnit\Framework\TestCase') && class_exists('\PHPUnit_Frame
|
||||
// Needed to let Cache work
|
||||
$GLOBALS['egw_info']['server']['temp_dir'] = '/tmp';
|
||||
$GLOBALS['egw_info']['server']['install_id'] = 'PHPUnit test';
|
||||
// setting a working session.save_path
|
||||
if (ini_get('session.save_handler') === 'files' && !is_writable(ini_get('session.save_path')) &&
|
||||
is_dir('/tmp') && is_writable('/tmp'))
|
||||
{
|
||||
ini_set('session.save_path','/tmp'); // regular users may have no rights to apache's session dir
|
||||
}
|
||||
// set domain from doc/phpunit.xml
|
||||
if (!isset($_SERVER['HTTP_HOST']) && $GLOBALS['EGW_DOMAIN'] !== 'default')
|
||||
{
|
||||
$_SERVER['HTTP_HOST'] = $GLOBALS['EGW_DOMAIN'];
|
||||
}
|
||||
|
||||
// Symlink api/src/fixtures/apps/* to root
|
||||
foreach(scandir($path=__DIR__.'/../api/tests/fixtures/apps') as $app)
|
||||
|
27
doc/travis-ci-apache.conf
Normal file
27
doc/travis-ci-apache.conf
Normal file
@ -0,0 +1,27 @@
|
||||
<VirtualHost *:80>
|
||||
# https://docs.travis-ci.com/user/languages/php/#apache--php
|
||||
|
||||
DocumentRoot %TRAVIS_BUILD_DIR%
|
||||
|
||||
# tests assume EGroupware to be under /egroupware not docroot
|
||||
Alias /egroupware %TRAVIS_BUILD_DIR%
|
||||
|
||||
<Directory "%TRAVIS_BUILD_DIR%/">
|
||||
Options FollowSymLinks MultiViews ExecCGI
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
# Wire up Apache to use Travis CI's php-fpm.
|
||||
<IfModule mod_fastcgi.c>
|
||||
AddHandler php7-fcgi .php
|
||||
Action php7-fcgi /php7-fcgi
|
||||
Alias /php7-fcgi /usr/lib/cgi-bin/php7-fcgi
|
||||
FastCgiExternalServer /usr/lib/cgi-bin/php7-fcgi -host 127.0.0.1:9000 -pass-header Authorization
|
||||
|
||||
<Directory /usr/lib/cgi-bin>
|
||||
Require all granted
|
||||
</Directory>
|
||||
</IfModule>
|
||||
|
||||
</VirtualHost>
|
@ -11,11 +11,10 @@
|
||||
*/
|
||||
|
||||
use EGroupware\Api;
|
||||
use EGroupware\Api\Link;
|
||||
use EGroupware\Api\Framework;
|
||||
use EGroupware\Api\Egw;
|
||||
use EGroupware\Api\Etemplate;
|
||||
|
||||
use EGroupware\Api\Framework;
|
||||
use EGroupware\Api\Link;
|
||||
use EGroupware\Api\Vfs;
|
||||
|
||||
/**
|
||||
@ -1023,6 +1022,10 @@ class filemanager_ui
|
||||
{
|
||||
$vfs_options['mime'] = $query['col_filter']['mime'];
|
||||
}
|
||||
if($namefilter)
|
||||
{
|
||||
$vfs_options['name'] = $query['search'];
|
||||
}
|
||||
|
||||
return $vfs_options;
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ class ContactTest extends \EGroupware\Api\AppTest
|
||||
// Infolog under test
|
||||
protected $info_id = null;
|
||||
|
||||
public function setUp()
|
||||
protected function setUp() : void
|
||||
{
|
||||
$this->ui = new \infolog_ui();
|
||||
|
||||
@ -39,7 +39,7 @@ class ContactTest extends \EGroupware\Api\AppTest
|
||||
$this->mockTracking($this->bo, 'infolog_tracking');
|
||||
}
|
||||
|
||||
public function tearDown()
|
||||
protected function tearDown() : void
|
||||
{
|
||||
// Double delete to make sure it's gone, not preserved due to history setting
|
||||
if($this->info_id)
|
||||
|
@ -34,7 +34,7 @@ class SetProjectManagerTest extends \EGroupware\Api\AppTest
|
||||
protected $pm_id = null;
|
||||
|
||||
|
||||
public function setUp()
|
||||
protected function setUp() : void
|
||||
{
|
||||
$this->ui = new \infolog_ui();
|
||||
|
||||
@ -63,7 +63,7 @@ class SetProjectManagerTest extends \EGroupware\Api\AppTest
|
||||
$this->makeProject();
|
||||
}
|
||||
|
||||
public function tearDown()
|
||||
protected function tearDown() : void
|
||||
{
|
||||
// Remove infolog under test
|
||||
if($this->info_id)
|
||||
|
@ -37,7 +37,7 @@ class StatusTest extends \EGroupware\Api\AppTest
|
||||
/**
|
||||
* Create a custom status we can use to test
|
||||
*/
|
||||
public static function setUpBeforeClass()
|
||||
public static function setUpBeforeClass() : void
|
||||
{
|
||||
parent::setUpBeforeClass();
|
||||
|
||||
@ -47,7 +47,7 @@ class StatusTest extends \EGroupware\Api\AppTest
|
||||
|
||||
Api\Config::save_value('status',$bo->status,'infolog');
|
||||
}
|
||||
public static function tearDownAfterClass()
|
||||
public static function tearDownAfterClass() : void
|
||||
{
|
||||
// Remove custom status
|
||||
$bo = new \infolog_bo();
|
||||
@ -58,14 +58,14 @@ class StatusTest extends \EGroupware\Api\AppTest
|
||||
parent::tearDownAfterClass();
|
||||
}
|
||||
|
||||
public function setUp()
|
||||
protected function setUp() : void
|
||||
{
|
||||
$this->bo = new \infolog_bo();
|
||||
|
||||
$this->mockTracking($this->bo, 'infolog_tracking');
|
||||
}
|
||||
|
||||
public function tearDown()
|
||||
protected function tearDown() : void
|
||||
{
|
||||
$this->bo = null;
|
||||
}
|
||||
|
@ -142,7 +142,7 @@ else
|
||||
$passwd_type = $_POST['passwd_type'];
|
||||
|
||||
// forced password change
|
||||
if($GLOBALS['egw']->session->cd_reason != Api\Session::CD_FORCE_PASSWORD_CHANGE)
|
||||
if($GLOBALS['egw']->session->cd_reason == Api\Session::CD_FORCE_PASSWORD_CHANGE)
|
||||
{
|
||||
// no automatic login
|
||||
}
|
||||
|
@ -446,6 +446,9 @@ div.mail-compose_fileselector {
|
||||
position: relative;
|
||||
background-color: white;
|
||||
}
|
||||
div#mail-index_mailPreview > div {
|
||||
padding-left: 8px !important;
|
||||
}
|
||||
#mail-index_mailPreview .et2_email>span{
|
||||
display: inline;
|
||||
}
|
||||
|
@ -72,10 +72,10 @@ tr.mail.labelone td:first-child {
|
||||
border-left: 6px solid #ff0080 !important;
|
||||
}
|
||||
tr.mail.labeltwo td:first-child {
|
||||
border-left: 6px solid #ff8000 !important;
|
||||
border-left: 6px solid #ff8000 !important;
|
||||
}
|
||||
tr.mail.labelthree td:first-child {
|
||||
border-left: 6px solid #008000 !important;
|
||||
border-left: 6px solid #008000 !important;
|
||||
}
|
||||
tr.mail.labelfour td:first-child {
|
||||
border-left: 6px solid #0000ff !important;
|
||||
@ -456,6 +456,9 @@ div.mail-compose_fileselector {
|
||||
position: relative;
|
||||
background-color: white;
|
||||
}
|
||||
div#mail-index_mailPreview > div {
|
||||
padding-left: 8px !important;
|
||||
}
|
||||
#mail-index_mailPreview .et2_email > span {
|
||||
display: inline;
|
||||
}
|
||||
@ -469,7 +472,7 @@ div.mail-compose_fileselector {
|
||||
border: none;
|
||||
}
|
||||
#mail-compose_composeToolbar > button {
|
||||
padding: .2em .4em;
|
||||
padding: 0.2em 0.4em;
|
||||
}
|
||||
#mail-compose_composeToolbar > img {
|
||||
width: 16px;
|
||||
@ -482,7 +485,7 @@ div.mail-compose_fileselector {
|
||||
height: 16px !important;
|
||||
}
|
||||
#mail-compose_composeToolbar > button {
|
||||
padding: .2em .4em;
|
||||
padding: 0.2em 0.4em;
|
||||
}
|
||||
#mail-compose_to div.ms-sel-ctn,
|
||||
#mail-compose_cc .ms-sel-ctn,
|
||||
@ -490,7 +493,7 @@ div.mail-compose_fileselector {
|
||||
max-height: 75px;
|
||||
}
|
||||
#mail-display_toolbar > button > span {
|
||||
padding: .2em .4em;
|
||||
padding: 0.2em 0.4em;
|
||||
}
|
||||
#mail-display_toolbar > button > span > img {
|
||||
width: 16px;
|
||||
@ -513,7 +516,7 @@ div.mail-compose_fileselector {
|
||||
height: 35px;
|
||||
}
|
||||
#mail-index_toolbar > button > span {
|
||||
padding: .2em .4em;
|
||||
padding: 0.2em 0.4em;
|
||||
}
|
||||
#mail-index_toolbar > button > span > img {
|
||||
width: 16px;
|
||||
@ -1649,11 +1652,11 @@ tr.mail.deleted td:first-child {
|
||||
}
|
||||
span.status_img {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url(../pixelegg/images/kmmsgread.svg);
|
||||
background-size: 12px 12px;
|
||||
background-size: 16px 16px;
|
||||
}
|
||||
tr.deleted span.status_img {
|
||||
background-image: url(../pixelegg/images/kmmsgdel.svg);
|
||||
@ -1668,7 +1671,7 @@ tr.flagged_unseen span.status_img {
|
||||
background-image: url(../pixelegg/images/unread_flagged_small.svg) !important;
|
||||
}
|
||||
tr.recent span.status_img {
|
||||
background-image: url(../pixelegg/images/kmmsgnew.png) !important;
|
||||
background-image: url(../pixelegg/images/kmmsgnew.svg) !important;
|
||||
}
|
||||
tr.replied span.status_img {
|
||||
background-image: url(../pixelegg/images/mail_reply.svg) !important;
|
||||
@ -1890,7 +1893,7 @@ input[type=button] {
|
||||
padding: 0px;
|
||||
}
|
||||
#mail-display_toolbar > button > span {
|
||||
padding: .2em .4em;
|
||||
padding: 0.2em 0.4em;
|
||||
}
|
||||
#mail-display_toolbar > button > span > img {
|
||||
width: 16px;
|
||||
|
@ -444,6 +444,9 @@ div.mail-compose_fileselector {
|
||||
position: relative;
|
||||
background-color: white;
|
||||
}
|
||||
div#mail-index_mailPreview > div {
|
||||
padding-left: 8px !important;
|
||||
}
|
||||
#mail-index_mailPreview .et2_email > span {
|
||||
display: inline;
|
||||
}
|
||||
|
@ -18,23 +18,23 @@ class SaveToVfsTest extends \PHPUnit\Framework\TestCase
|
||||
/**
|
||||
* Create a custom status we can use to test
|
||||
*/
|
||||
public static function setUpBeforeClass()
|
||||
public static function setUpBeforeClass() : void
|
||||
{
|
||||
parent::setUpBeforeClass();
|
||||
|
||||
}
|
||||
public static function tearDownAfterClass()
|
||||
public static function tearDownAfterClass() : void
|
||||
{
|
||||
|
||||
// Have to remove custom status first, before the DB is gone
|
||||
parent::tearDownAfterClass();
|
||||
}
|
||||
|
||||
public function setUp()
|
||||
protected function setUp() : void
|
||||
{
|
||||
}
|
||||
|
||||
public function tearDown()
|
||||
protected function tearDown() : void
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -1809,7 +1809,6 @@ div#loginMainDiv.stockLoginBackground div#centerBox form {
|
||||
background: white;
|
||||
opacity: 0.94;
|
||||
margin-left: 20px;
|
||||
border-radius: 5px;
|
||||
margin-right: 20px;
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
@ -1888,7 +1887,6 @@ div#loginMainDiv.stockLoginBackground div#centerBox form {
|
||||
width: 280px;
|
||||
}
|
||||
#loginMainDiv div#centerBox form {
|
||||
border-radius: 5px;
|
||||
opacity: 0.94;
|
||||
background-color: white;
|
||||
padding: 1em;
|
||||
|
@ -96,7 +96,6 @@ div#loginMainDiv.stockLoginBackground {
|
||||
background: white;
|
||||
opacity: 0.94;
|
||||
margin-left: 20px;
|
||||
border-radius: 5px;
|
||||
margin-right: 20px;
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
@ -184,7 +183,6 @@ div#loginMainDiv.stockLoginBackground {
|
||||
|
||||
// Formular
|
||||
form {
|
||||
border-radius:5px;
|
||||
opacity:0.94;
|
||||
background-color: white;
|
||||
padding:1em;
|
||||
|
@ -253,6 +253,7 @@ class setup
|
||||
case PHP_SESSION_DISABLED:
|
||||
throw new \ErrorException('EGroupware requires PHP session extension!');
|
||||
case PHP_SESSION_NONE:
|
||||
if (headers_sent()) return true;
|
||||
ini_set('session.use_cookie', true);
|
||||
session_name(self::SESSIONID);
|
||||
session_set_cookie_params(0, '/', self::cookiedomain(),
|
||||
|
@ -25,9 +25,9 @@ $GLOBALS['egw_info'] = array(
|
||||
'currentapp' => 'setup',
|
||||
'noapi' => True
|
||||
));
|
||||
if(file_exists('../header.inc.php'))
|
||||
if(file_exists(__DIR__.'/../../header.inc.php'))
|
||||
{
|
||||
include('../header.inc.php');
|
||||
include_once(__DIR__.'/../../header.inc.php');
|
||||
}
|
||||
// for an old header we need to setup a reference for the domains
|
||||
if (!is_array($GLOBALS['egw_domain'])) $GLOBALS['egw_domain'] =& $GLOBALS['phpgw_domain'];
|
||||
|
Loading…
Reference in New Issue
Block a user