From 4c6e41d4790e401ea9464a5db0f5a266f10e9fbf Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Wed, 4 Mar 2020 16:58:38 +0100 Subject: [PATCH] * CalDAV/OutlookSynchronizer: reject invitations when client deletes then without appropriate rights in his calendar --- .travis.yml | 36 +- api/src/Accounts.php | 2 +- api/src/Accounts/Ldap.php | 2 +- api/src/Cache.php | 7 +- api/src/Cache/Memcache.php | 3 + api/src/Session.php | 2 + api/src/Translation.php | 6 +- api/tests/CalDAVTest.php | 375 ++++ calendar/inc/class.calendar_bo.inc.php | 2 +- calendar/inc/class.calendar_groupdav.inc.php | 57 +- .../tests/CalDAV/CalDAVcreateReadDelete.php | 121 ++ calendar/tests/CalDAV/CalDAVsingleDELETE.php | 368 ++++ composer.json | 2 + composer.lock | 1749 ++++++++++++++++- doc/travis-ci-apache.conf | 27 + setup/inc/class.setup.inc.php | 1 + setup/inc/functions.inc.php | 17 +- 17 files changed, 2724 insertions(+), 53 deletions(-) create mode 100644 api/tests/CalDAVTest.php create mode 100644 calendar/tests/CalDAV/CalDAVcreateReadDelete.php create mode 100644 calendar/tests/CalDAV/CalDAVsingleDELETE.php create mode 100644 doc/travis-ci-apache.conf diff --git a/.travis.yml b/.travis.yml index 0336bfca77..c14a093d11 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,25 +13,15 @@ matrix: 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,15 +31,23 @@ before_script: echo "extension=memcached.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; ;; 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 + - sudo service apache2 restart - case $(phpenv version-name) in - "5.6") - composer require 'phpunit/phpunit:~5.7'; - ;; - "7"|"7.0") - composer require 'phpunit/phpunit:~6'; - ;; + "7.2") + composer require --ignore-platform-reqs 'phpunit/phpunit:~8'; + ;; *) - composer require --ignore-platform-reqs 'phpunit/phpunit:~7'; + composer require --ignore-platform-reqs 'phpunit/phpunit:~9'; ;; esac - php -m diff --git a/api/src/Accounts.php b/api/src/Accounts.php index 2c7b6541a8..15f2b6db61 100644 --- a/api/src/Accounts.php +++ b/api/src/Accounts.php @@ -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)) { diff --git a/api/src/Accounts/Ldap.php b/api/src/Accounts/Ldap.php index ddd4303fc3..f18f898449 100644 --- a/api/src/Accounts/Ldap.php +++ b/api/src/Accounts/Ldap.php @@ -125,7 +125,7 @@ class Ldap * * @var Api\Accounts */ - private $frontend; + protected $frontend; /** * Instance of the ldap class diff --git a/api/src/Cache.php b/api/src/Cache.php index ba34282990..c859e0f6a5 100644 --- a/api/src/Cache.php +++ b/api/src/Cache.php @@ -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)); diff --git a/api/src/Cache/Memcache.php b/api/src/Cache/Memcache.php index 8523e92d50..d1f3819b0a 100644 --- a/api/src/Cache/Memcache.php +++ b/api/src/Cache/Memcache.php @@ -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 * diff --git a/api/src/Session.php b/api/src/Session.php index c800446693..17dc2ec68e 100644 --- a/api/src/Session.php +++ b/api/src/Session.php @@ -1658,6 +1658,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 diff --git a/api/src/Translation.php b/api/src/Translation.php index bc5381793a..59fa77fb23 100644 --- a/api/src/Translation.php +++ b/api/src/Translation.php @@ -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, diff --git a/api/tests/CalDAVTest.php b/api/tests/CalDAVTest.php new file mode 100644 index 0000000000..426459cdfe --- /dev/null +++ b/api/tests/CalDAVTest.php @@ -0,0 +1,375 @@ + + * @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: + * + * $this->users = [ + * 'boss' => [], + * 'secretary' => ['rights' => ['boss' => Acl::READ|Acl::ADD|Acl::EDIT|Acl::DELETE]], + * 'other' => [], + * ]; + * $this->createUsersACL($this->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'] = $this->createUser($user, $data); + + foreach($data['rights'] ?? [] as $grantee => $rights) + { + $this->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 $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 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'] = $this->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! + $this->addAcl(['groupdav','calendar','infolog','addressbook'], 'run', $data['id']); + + $this->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($this->created_users[$_account_lid])) + { + throw new \InvalidArgumentException("No user '$_account_lid' exist, need to create it with createUser('$_account_lid')"); + } + else + { + $password = $this->created_users[$_account_lid]['password']; + } + return [RequestOptions::AUTH => [$_account_lid, $password]]; + } + + /** + * Tear down: + * - delete users created by createUser() incl. their ACL and data + */ + public function tearDown() + { + $setup = $this->getSetup(); + + foreach($this->created_users as $account_lid => $data) + { +// if ($id) $setup->accounts->delete($data['id']); + unset($this->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 + */ + function addAcl($apps, $location, $account, $rights=1) + { + return $this->getSetup()->add_acl($apps, $location, $account, $rights); + } + + /** + * Return instance of setup object eg. to create users + * + * @return \setup + */ + private function getSetup() + { + if (!isset($_REQUEST['domain'])) + { + $_REQUEST['domain'] = $GLOBALS['EGW_DOMAIN'] ?? 'default'; + } + $_REQUEST['ConfigDomain'] = $_REQUEST['domain']; + require_once __DIR__.'/../../setup/inc/functions.inc.php'; + + return $GLOBALS['egw_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'=>[[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; + } + + public static function setUpBeforeClass() + { + parent::setUpBeforeClass(); + } + + public static function tearDownAfterClass() + { + parent::tearDownAfterClass(); + } + + public function setUp() + { + + } +} \ No newline at end of file diff --git a/calendar/inc/class.calendar_bo.inc.php b/calendar/inc/class.calendar_bo.inc.php index a275c8e7fc..a359103607 100644 --- a/calendar/inc/class.calendar_bo.inc.php +++ b/calendar/inc/class.calendar_bo.inc.php @@ -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)) diff --git a/calendar/inc/class.calendar_groupdav.inc.php b/calendar/inc/class.calendar_groupdav.inc.php index aeb87ad367..59d1c04502 100644 --- a/calendar/inc/class.calendar_groupdav.inc.php +++ b/calendar/inc/class.calendar_groupdav.inc.php @@ -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; diff --git a/calendar/tests/CalDAV/CalDAVcreateReadDelete.php b/calendar/tests/CalDAV/CalDAVcreateReadDelete.php new file mode 100644 index 0000000000..4ccdf12e95 --- /dev/null +++ b/calendar/tests/CalDAV/CalDAVcreateReadDelete.php @@ -0,0 +1,121 @@ + + * @package calendar + * @subpackage tests + * @copyright (c) 2020 by Ralf Becker + * @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 = <<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); + } +} \ No newline at end of file diff --git a/calendar/tests/CalDAV/CalDAVsingleDELETE.php b/calendar/tests/CalDAV/CalDAVsingleDELETE.php new file mode 100644 index 0000000000..d5af90eb96 --- /dev/null +++ b/calendar/tests/CalDAV/CalDAVsingleDELETE.php @@ -0,0 +1,368 @@ + + * @package calendar + * @subpackage tests + * @copyright (c) 2020 by Ralf Becker + * @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 $users = [ + 'boss' => [], + 'secretary' => [ + 'rights' => [ + 'boss' => Acl::READ|Acl::ADD|Acl::EDIT|Acl::DELETE, + ] + ], + 'other' => [], + ]; + + /** + * Create some users incl. ACL + */ + public function setUp() + { + parent::setUp(); + + $this->createUsersACL($this->users, 'calendar'); + } + + /** + * Check created users + */ + public function testPrincipals() + { + foreach($this->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 = <<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 = <<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 = <<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"); + } +} \ No newline at end of file diff --git a/composer.json b/composer.json index 4aa102eeda..46e5d3c13c 100644 --- a/composer.json +++ b/composer.json @@ -101,6 +101,8 @@ "tinymce/tinymce": "^5.0" }, "require-dev": { + "guzzlehttp/guzzle": "^6.5", + "phpunit/phpunit": "^6.5" }, "suggests": { "ext-opcache": "Opcode cache to speed up PHP", diff --git a/composer.lock b/composer.lock index 2bf77c5761..a546e3def5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0f9cd7cf5343e5c411b2e04d75b2fa71", + "content-hash": "27faeaf9e9bc34e41c89222565e6db06", "packages": [ { "name": "adldap2/adldap2", @@ -575,6 +575,7 @@ ], "description": "Promoting the interoperability of container objects (DIC, SL, etc.)", "homepage": "https://github.com/container-interop/container-interop", + "abandoned": "psr/container", "time": "2017-02-14T19:40:03+00:00" }, { @@ -3972,10 +3973,1754 @@ "psr", "psr-7" ], + "abandoned": "laminas/laminas-diactoros", "time": "2018-09-05T19:29:37+00:00" } ], - "packages-dev": [], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "shasum": "" + }, + "require": { + "php": ">=5.3,<8.0-DEV" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "ext-pdo": "*", + "ext-phar": "*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://github.com/doctrine/instantiator", + "keywords": [ + "constructor", + "instantiate" + ], + "time": "2015-06-14T21:17:01+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "6.5.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "43ece0e75098b7ecd8d13918293029e555a50f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/43ece0e75098b7ecd8d13918293029e555a50f82", + "reference": "43ece0e75098b7ecd8d13918293029e555a50f82", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.6.1", + "php": ">=5.5" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", + "psr/log": "^1.1" + }, + "suggest": { + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.5-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2019-12-23T11:57:10+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "v1.3.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "shasum": "" + }, + "require": { + "php": ">=5.5.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "time": "2016-12-20T10:07:11+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.6.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "239400de7a173fe9901b9ac7c06497751f00727a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/239400de7a173fe9901b9ac7c06497751f00727a", + "reference": "239400de7a173fe9901b9ac7c06497751f00727a", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "ext-zlib": "*", + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" + }, + "suggest": { + "zendframework/zend-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "time": "2019-07-01T23:21:34+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", + "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "doctrine/collections": "^1.0", + "doctrine/common": "^2.6", + "phpunit/phpunit": "^4.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + }, + "files": [ + "src/DeepCopy/deep_copy.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "time": "2017-10-19T19:58:43+00:00" + }, + { + "name": "phar-io/manifest", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/2df402786ab5368a0169091f61a7c1e0eb6852d0", + "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "phar-io/version": "^1.0.1", + "php": "^5.6 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "time": "2017-03-05T18:14:27+00:00" + }, + { + "name": "phar-io/version", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/a70c0ced4be299a63d32fa96d9281d03e94041df", + "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "time": "2017-03-05T17:38:23+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", + "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "time": "2017-09-11T18:02:19+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "4.3.4", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "da3fd972d6bafd628114f7e7e036f45944b62e9c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/da3fd972d6bafd628114f7e7e036f45944b62e9c", + "reference": "da3fd972d6bafd628114f7e7e036f45944b62e9c", + "shasum": "" + }, + "require": { + "php": "^7.0", + "phpdocumentor/reflection-common": "^1.0.0 || ^2.0.0", + "phpdocumentor/type-resolver": "~0.4 || ^1.0.0", + "webmozart/assert": "^1.0" + }, + "require-dev": { + "doctrine/instantiator": "^1.0.5", + "mockery/mockery": "^1.0", + "phpdocumentor/type-resolver": "0.4.*", + "phpunit/phpunit": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "time": "2019-12-28T18:55:12+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "0.5.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "cf842904952e64e703800d094cdf34e715a8a3ae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/cf842904952e64e703800d094cdf34e715a8a3ae", + "reference": "cf842904952e64e703800d094cdf34e715a8a3ae", + "shasum": "" + }, + "require": { + "php": "^7.0", + "phpdocumentor/reflection-common": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "time": "2017-12-30T13:23:38+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "v1.10.2", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "b4400efc9d206e83138e2bb97ed7f5b14b831cd9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/b4400efc9d206e83138e2bb97ed7f5b14b831cd9", + "reference": "b4400efc9d206e83138e2bb97ed7f5b14b831cd9", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.3|^7.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", + "sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0", + "sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0" + }, + "require-dev": { + "phpspec/phpspec": "^2.5 || ^3.2", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10.x-dev" + } + }, + "autoload": { + "psr-4": { + "Prophecy\\": "src/Prophecy" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2020-01-20T15:57:02+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "5.3.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "c89677919c5dd6d3b3852f230a663118762218ac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c89677919c5dd6d3b3852f230a663118762218ac", + "reference": "c89677919c5dd6d3b3852f230a663118762218ac", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlwriter": "*", + "php": "^7.0", + "phpunit/php-file-iterator": "^1.4.2", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-token-stream": "^2.0.1", + "sebastian/code-unit-reverse-lookup": "^1.0.1", + "sebastian/environment": "^3.0", + "sebastian/version": "^2.0.1", + "theseer/tokenizer": "^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "suggest": { + "ext-xdebug": "^2.5.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2018-04-06T15:36:58+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "1.4.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2017-11-27T13:52:08+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2015-06-21T13:50:34+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "1.0.9", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2017-02-26T11:10:40+00:00" + }, + { + "name": "phpunit/php-token-stream", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "791198a2c6254db10131eecfe8c06670700904db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/791198a2c6254db10131eecfe8c06670700904db", + "reference": "791198a2c6254db10131eecfe8c06670700904db", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.2.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "time": "2017-11-27T05:48:46+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "6.5.14", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "bac23fe7ff13dbdb461481f706f0e9fe746334b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/bac23fe7ff13dbdb461481f706f0e9fe746334b7", + "reference": "bac23fe7ff13dbdb461481f706f0e9fe746334b7", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "myclabs/deep-copy": "^1.6.1", + "phar-io/manifest": "^1.0.1", + "phar-io/version": "^1.0", + "php": "^7.0", + "phpspec/prophecy": "^1.7", + "phpunit/php-code-coverage": "^5.3", + "phpunit/php-file-iterator": "^1.4.3", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-timer": "^1.0.9", + "phpunit/phpunit-mock-objects": "^5.0.9", + "sebastian/comparator": "^2.1", + "sebastian/diff": "^2.0", + "sebastian/environment": "^3.1", + "sebastian/exporter": "^3.1", + "sebastian/global-state": "^2.0", + "sebastian/object-enumerator": "^3.0.3", + "sebastian/resource-operations": "^1.0", + "sebastian/version": "^2.0.1" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "3.0.2", + "phpunit/dbunit": "<3.0" + }, + "require-dev": { + "ext-pdo": "*" + }, + "suggest": { + "ext-xdebug": "*", + "phpunit/php-invoker": "^1.1" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.5.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2019-02-01T05:22:47+00:00" + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "5.0.10", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/cd1cf05c553ecfec36b170070573e540b67d3f1f", + "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.5", + "php": "^7.0", + "phpunit/php-text-template": "^1.2.1", + "sebastian/exporter": "^3.1" + }, + "conflict": { + "phpunit/phpunit": "<6.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.5.11" + }, + "suggest": { + "ext-soap": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "keywords": [ + "mock", + "xunit" + ], + "abandoned": true, + "time": "2018-08-09T05:50:03+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "time": "2017-03-04T06:30:41+00:00" + }, + { + "name": "sebastian/comparator", + "version": "2.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/34369daee48eafb2651bea869b4b15d75ccc35f9", + "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9", + "shasum": "" + }, + "require": { + "php": "^7.0", + "sebastian/diff": "^2.0 || ^3.0", + "sebastian/exporter": "^3.1" + }, + "require-dev": { + "phpunit/phpunit": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2018-02-01T13:46:46+00:00" + }, + { + "name": "sebastian/diff", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", + "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ], + "time": "2017-08-03T08:09:46+00:00" + }, + { + "name": "sebastian/environment", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/cd0871b3975fb7fc44d11314fd1ee20925fce4f5", + "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "time": "2017-07-01T08:51:00+00:00" + }, + { + "name": "sebastian/exporter", + "version": "3.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e", + "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e", + "shasum": "" + }, + "require": { + "php": "^7.0", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2019-09-14T09:02:43+00:00" + }, + { + "name": "sebastian/global-state", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2017-04-27T15:39:26+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5", + "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5", + "shasum": "" + }, + "require": { + "php": "^7.0", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "time": "2017-08-03T12:35:26+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "773f97c67f28de00d397be301821b06708fca0be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be", + "reference": "773f97c67f28de00d397be301821b06708fca0be", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "time": "2017-03-29T09:07:27+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "time": "2017-03-03T06:23:57+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "shasum": "" + }, + "require": { + "php": ">=5.6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "time": "2015-07-28T20:34:47+00:00" + }, + { + "name": "sebastian/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2016-10-03T07:35:21+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.14.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "fbdeaec0df06cf3d51c93de80c7eb76e271f5a38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/fbdeaec0df06cf3d51c93de80c7eb76e271f5a38", + "reference": "fbdeaec0df06cf3d51c93de80c7eb76e271f5a38", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.14-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "time": "2020-01-13T11:15:53+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9", + "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "time": "2019-06-13T22:48:21+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "aed98a490f9a8f78468232db345ab9cf606cf598" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/aed98a490f9a8f78468232db345ab9cf606cf598", + "reference": "aed98a490f9a8f78468232db345ab9cf606cf598", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "vimeo/psalm": "<3.6.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.36 || ^7.5.13" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "time": "2020-02-14T12:15:55+00:00" + } + ], "aliases": [], "minimum-stability": "dev", "stability-flags": { diff --git a/doc/travis-ci-apache.conf b/doc/travis-ci-apache.conf new file mode 100644 index 0000000000..1703920bb4 --- /dev/null +++ b/doc/travis-ci-apache.conf @@ -0,0 +1,27 @@ + + # 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% + + + Options FollowSymLinks MultiViews ExecCGI + AllowOverride All + Require all granted + + + # Wire up Apache to use Travis CI's php-fpm. + + AddHandler php5-fcgi .php + Action php5-fcgi /php5-fcgi + Alias /php5-fcgi /usr/lib/cgi-bin/php5-fcgi + FastCgiExternalServer /usr/lib/cgi-bin/php5-fcgi -host 127.0.0.1:9000 -pass-header Authorization + + + Require all granted + + + + \ No newline at end of file diff --git a/setup/inc/class.setup.inc.php b/setup/inc/class.setup.inc.php index e332a071ac..fc0013785b 100644 --- a/setup/inc/class.setup.inc.php +++ b/setup/inc/class.setup.inc.php @@ -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 false; ini_set('session.use_cookie', true); session_name(self::SESSIONID); session_set_cookie_params(0, '/', self::cookiedomain(), diff --git a/setup/inc/functions.inc.php b/setup/inc/functions.inc.php index e24cbd8a9f..b8261eebee 100644 --- a/setup/inc/functions.inc.php +++ b/setup/inc/functions.inc.php @@ -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']; @@ -56,14 +56,17 @@ require_once(EGW_INCLUDE_ROOT . '/api/src/loader/common.php'); * function to handle multilanguage support * */ -function lang($key,$vars=null) +if (!function_exists('lang')) { - if(!is_array($vars)) + function lang($key, $vars = null) { - $vars = func_get_args(); - array_shift($vars); // remove $key + if (!is_array($vars)) + { + $vars = func_get_args(); + array_shift($vars); // remove $key + } + return $GLOBALS['egw_setup']->translation->translate("$key", $vars); } - return $GLOBALS['egw_setup']->translation->translate("$key", $vars); } if(file_exists(EGW_SERVER_ROOT.'/api/setup/setup.inc.php'))