/** * Redirect to the installation page of the application. * This method does not send the response. * * @param Response $response HTTP response. * @param Request $request HTTP request. * @return void */ static function redirectToInstall(Response $response, Request $request) { list($dirname) = Uri\split($request->getUrl()); $response->setStatus(307); $response->setHeader('Location', $dirname . '/install.php'); $response->setBody('The application is not installed. ' . 'You are going to be redirected to the installation page.'); }
/** * Returns a principals' collection of files. * * The passed array contains principal information, and is guaranteed to * at least contain a uri item. Other properties may or may not be * supplied by the authentication backend. * * @param array $principalInfo * @return void */ function getChildForPrincipal(array $principalInfo) { $owner = $principalInfo['uri']; $acl = [['privilege' => '{DAV:}read', 'principal' => $owner, 'protected' => true], ['privilege' => '{DAV:}write', 'principal' => $owner, 'protected' => true]]; list(, $principalBaseName) = Uri\split($owner); $path = $this->storagePath . '/' . $principalBaseName; if (!is_dir($path)) { mkdir($path, 0777, true); } return new Collection($path, $acl, $owner); }
/** * Returns a principals' collection of files. * * The passed array contains principal information, and is guaranteed to * at least contain a uri item. Other properties may or may not be * supplied by the authentication backend. * * @param array $principalInfo * @return void */ function getChildForPrincipal(array $principalInfo) { $owner = $principalInfo['uri']; $acl = [['privilege' => '{DAV:}read', 'principal' => $owner, 'protected' => true], ['privilege' => '{DAV:}write', 'principal' => $owner, 'protected' => true]]; list(, $principalBaseName) = SabreUri\split($owner); $path = $this->storagePath . DS . $principalBaseName; if (!is_dir($path)) { HoaFile\Directory::create($path, HoaFile\Directory::MODE_CREATE_RECURSIVE); } $public = $path . DS . 'public'; if (!is_dir($public)) { HoaFile\Directory::create($public, HoaFile\Directory::MODE_CREATE_RECURSIVE); } $out = new Directory($path, $acl, $owner); $out->setRelativePath($this->storagePath); return $out; }
/** * @param string $prefixPath * * @return array|null */ public function getPrincipalsByPrefix($prefixPath) { $dbPrincipals = $this->manager->findAll('public', 'principal'); if ($dbPrincipals->count() == 0) { return null; } $principals = []; foreach ($dbPrincipals as $dbPrincipal) { // Checking if the principal is in the prefix list($rowPrefix, $basename) = Uri\split($dbPrincipal->uri); if ($rowPrefix !== $prefixPath) { continue; } $principal = ['uri' => $dbPrincipal->uri]; foreach ($this->fieldMap as $key => $value) { $principal[$key] = $dbPrincipal->{$value}['dbField']; } $principals[] = $principal; } return $principals; }
/** * Check if the current path is inside the `public/` directory of the * current principal. * * @return bool */ function isPublic() { list(, $principalBaseName) = SabreUri\split($this->owner); $publicPath = $this->getRelativePath() . DS . $principalBaseName . DS . 'public'; return $publicPath === substr($this->path, 0, min(strlen($publicPath), strlen($this->path))); }
/** * Main method. * * @return int */ function main() { $operation = 0; $location = null; $updateServer = Updater::DEFAULT_UPDATE_SERVER; while (false !== ($c = $this->getOption($v))) { switch ($c) { case '__ambiguous': $this->resolveOptionAmbiguity($v); break; case 'f': $operation = static::OPERATION_FETCH | Updater::FORMAT_PHAR; break; case 'z': $operation = static::OPERATION_FETCH | Updater::FORMAT_ZIP; break; case 'a': $operation = static::OPERATION_APPLY; $location = $v; break; case 's': $updateServer = $v; break; case 'h': case '?': default: return $this->usage(); break; } } $updateServer = rtrim($updateServer, '/') . '/'; if (0 !== (static::OPERATION_FETCH & $operation)) { $updatesDotJson = Updater::getUpdateUrl($updateServer); $versions = @file_get_contents($updatesDotJson); if (empty($versions)) { throw new Exception\Console('Oh no! We are not able to check if a new version exists… ' . 'Contact us at http://sabre.io/ ' . '(tried URL %s).', 0, $updatesDotJson); } $versions = json_decode($versions, true); /** * Expected format: * { * "1.0.1": { * "phar": "https://…", * "zip" : "https://…" * }, * "1.0.0": { * "phar": "https://…", * "zip" : "https://…" * }, * … * } */ $versionsToFetch = Updater::filterVersions($versions, Version::VERSION, $operation); $windowWidth = Window::getSize()['x']; $progress = function ($percent) use($windowWidth) { Cursor::clear('↔'); $message = 'Downloading… '; $barWidth = $windowWidth - mb_strlen($message); if ($percent <= 0) { $color = '#c74844'; } elseif ($percent <= 25) { $color = '#cb9a3d'; } elseif ($percent <= 50) { $color = '#dcb11e'; } elseif ($percent <= 75) { $color = '#aed633'; } else { $color = '#54b455'; } echo $message; Cursor::colorize('foreground(' . $color . ') background(' . $color . ')'); echo str_repeat('|', $percent * $barWidth / 100); Cursor::colorize('normal'); }; foreach ($versionsToFetch as $versionNumber => $versionUrl) { list(, $versionUrlBasename) = Uri\split($versionUrl); $fileIn = new File\Read($versionUrl, File::MODE_READ, null, true); $fileOut = new File\Write(SABRE_KATANA_PREFIX . '/data/share/update/' . $versionUrlBasename); echo "\n", 'Fetch version ', $versionNumber, ' from ', $versionUrl, "\n", 'Waiting…', "\n"; $fileIn->on('connect', function () { Cursor::clear('↔'); echo 'Downloading… '; }); $fileIn->on('progress', function (Core\Event\Bucket $bucket) use($progress) { static $previousPercent = 0; $data = $bucket->getData(); $current = $data['transferred']; $max = $data['max']; $percent = $current * 100 / $max; $delta = $percent - $previousPercent; if (1 <= $delta) { $previousPercent = $percent; $progress($percent); } }); $fileIn->open(); $fileOut->writeAll($fileIn->readAll()); echo "\n", 'Fetched at ', $fileOut->getStreamName(), '.', "\n"; } return 0; } elseif (static::OPERATION_APPLY === $operation) { if (false === file_exists($location)) { throw new Exception\Console('Update %s is not found.', 1, $location); } $processus = new Console\Processus(Core::getPHPBinary(), [$location, '--extract' => SABRE_KATANA_PREFIX, '--overwrite']); $processus->on('input', function () { return false; }); $processus->on('output', function (Core\Event\Bucket $bucket) { echo $bucket->getData()['line'], "\n"; }); $processus->run(); if (true === $processus->isSuccessful()) { echo 'sabre/katana updated!', "\n"; } else { echo 'Something wrong happened!', "\n"; } return $processus->getExitCode(); } else { return $this->usage(); } }
/** * Returns the name of this object * * @return string */ function getName() { list(, $name) = Uri\split($this->principalUri); return $name; }
/** * Checks if the submitted iCalendar data is in fact, valid. * * An exception is thrown if it's not. * * @param resource|string $data * @param string $path * @param bool $modified Should be set to true, if this event handler * changed &$data. * @param RequestInterface $request The http request. * @param ResponseInterface $response The http response. * @param bool $isNew Is the item a new one, or an update. * @return void */ protected function validateICalendar(&$data, $path, &$modified, RequestInterface $request, ResponseInterface $response, $isNew) { // If it's a stream, we convert it to a string first. if (is_resource($data)) { $data = stream_get_contents($data); } $before = md5($data); // Converting the data to unicode, if needed. $data = DAV\StringUtil::ensureUTF8($data); if ($before !== md5($data)) { $modified = true; } try { // If the data starts with a [, we can reasonably assume we're dealing // with a jCal object. if (substr($data, 0, 1) === '[') { $vobj = VObject\Reader::readJson($data); // Converting $data back to iCalendar, as that's what we // technically support everywhere. $data = $vobj->serialize(); $modified = true; } else { $vobj = VObject\Reader::read($data); } } catch (VObject\ParseException $e) { throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid iCalendar 2.0 data. Parse error: ' . $e->getMessage()); } if ($vobj->name !== 'VCALENDAR') { throw new DAV\Exception\UnsupportedMediaType('This collection can only support iCalendar objects.'); } $sCCS = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'; // Get the Supported Components for the target calendar list($parentPath) = Uri\split($path); $calendarProperties = $this->server->getProperties($parentPath, [$sCCS]); if (isset($calendarProperties[$sCCS])) { $supportedComponents = $calendarProperties[$sCCS]->getValue(); } else { $supportedComponents = ['VJOURNAL', 'VTODO', 'VEVENT']; } $foundType = null; $foundUID = null; foreach ($vobj->getComponents() as $component) { switch ($component->name) { case 'VTIMEZONE': continue 2; case 'VEVENT': case 'VTODO': case 'VJOURNAL': if (is_null($foundType)) { $foundType = $component->name; if (!in_array($foundType, $supportedComponents)) { throw new Exception\InvalidComponentType('This calendar only supports ' . implode(', ', $supportedComponents) . '. We found a ' . $foundType); } if (!isset($component->UID)) { throw new DAV\Exception\BadRequest('Every ' . $component->name . ' component must have an UID'); } $foundUID = (string) $component->UID; } else { if ($foundType !== $component->name) { throw new DAV\Exception\BadRequest('A calendar object must only contain 1 component. We found a ' . $component->name . ' as well as a ' . $foundType); } if ($foundUID !== (string) $component->UID) { throw new DAV\Exception\BadRequest('Every ' . $component->name . ' in this object must have identical UIDs'); } } break; default: throw new DAV\Exception\BadRequest('You are not allowed to create components of type: ' . $component->name . ' here'); } } if (!$foundType) { throw new DAV\Exception\BadRequest('iCalendar object must contain at least 1 of VEVENT, VTODO or VJOURNAL'); } // We use an extra variable to allow event handles to tell us wether // the object was modified or not. // // This helps us determine if we need to re-serialize the object. $subModified = false; $this->server->emit('calendarObjectChange', [$request, $response, $vobj, $parentPath, &$subModified, $isNew]); if ($subModified) { // An event handler told us that it modified the object. $data = $vobj->serialize(); // Using md5 to figure out if there was an *actual* change. if (!$modified && $before !== md5($data)) { $modified = true; } } }
/** * Main method. * * @return int */ function main() { $verbose = !(Console::isDirect(STDOUT) || !OS_WIN); while (false !== ($c = $this->getOption($v))) { switch ($c) { case '__ambiguous': $this->resolveOptionAmbiguity($v); break; case 'v': $verbose = $v; break; case 'h': case '?': default: return $this->usage(); } } if (true === Installer::isInstalled()) { echo 'The application is already installed.', "\n"; return 1; } $oldTitle = Window::getTitle(); Window::setTitle('Installation of sabre/katana'); $form = ['baseUrl' => '/', 'email' => null, 'password' => null, 'database' => ['driver' => 'sqlite', 'host' => '', 'port' => '', 'name' => '', 'username' => '', 'password' => '']]; $readline = new Console\Readline(); if (true === $verbose) { $windowWidth = Window::getSize()['x']; $labelMaxWidth = 35; $inputMaxWidth = $windowWidth - $labelMaxWidth; $numberOfSteps = 5; $input = function ($default = '') use($inputMaxWidth) { return Text::colorize($default . str_repeat(' ', $inputMaxWidth - mb_strlen($default)), 'foreground(black) background(#cccccc)'); }; $resetInput = function ($default = '') use($input, $labelMaxWidth) { Cursor::move('→', $labelMaxWidth); echo $input($default); Cursor::move('LEFT'); Cursor::move('→', $labelMaxWidth); Cursor::colorize('foreground(black) background(#cccccc)'); }; echo Text::colorize('Installation of sabre/' . "\n" . Welcome::LOGO, 'foreground(yellow)'), "\n\n", static::getBaseURLInfo(), "\n\n", 'Choose the base URL: ', $input('/'), "\n", 'Your administrator login: '******'Choose the administrator password: '******'Choose the administrator email: ', $input(), "\n", 'Choose the database driver: ', '🔘 SQLite ⚪️ MySQL', "\n"; Window::scroll('↑', 10); Cursor::move('↑', 10); Cursor::move('↑', $numberOfSteps); Cursor::move('→', $labelMaxWidth); // Disable arrow up and down. $no_echo = function ($readline) { return $readline::STATE_NO_ECHO; }; $readline->addMapping("[A", $no_echo); $readline->addMapping("[B", $no_echo); $step = function ($index, $label, callable $validator, $errorMessage, $default = '') use($numberOfSteps, &$readline, $resetInput, $labelMaxWidth) { Cursor::colorize('foreground(black) background(#cccccc)'); do { $out = $readline->readLine(); if (empty($out)) { $out = $default; } $valid = $validator($out); if (true !== $valid) { Cursor::move('↑'); $resetInput($default); Cursor::save(); Cursor::move('LEFT'); Cursor::move('↓', $numberOfSteps - $index + 1); list($title, $message) = explode("\n", $errorMessage); Cursor::colorize('foreground(white) background(red)'); echo $title, "\n"; Cursor::colorize('foreground(red) background(normal)'); echo $message; Cursor::restore(); } else { Cursor::save(); Cursor::move('LEFT'); Cursor::move('↓', $numberOfSteps - $index - 1); Cursor::colorize('normal'); Cursor::clear('↓'); Cursor::restore(); } } while (true !== $valid); if ($numberOfSteps !== $index + 1) { Cursor::move('→', $labelMaxWidth); } Cursor::colorize('normal'); return $out; }; $progress = function ($percent, $message) use($windowWidth) { static $margin = 4; $barWidth = $windowWidth - $margin * 2; Cursor::move('LEFT'); Cursor::move('↑', 1); Cursor::clear('↓'); if ($percent <= 0) { $color = '#c74844'; } elseif ($percent <= 25) { $color = '#cb9a3d'; } elseif ($percent <= 50) { $color = '#dcb11e'; } elseif ($percent <= 75) { $color = '#aed633'; } else { $color = '#54b455'; } echo str_repeat(' ', $margin); Cursor::colorize('foreground(' . $color . ') background(' . $color . ')'); echo str_repeat('|', $percent * $barWidth / 100); Cursor::move('LEFT ↓'); Cursor::colorize('background(normal)'); echo str_repeat(' ', $margin) . $message; Cursor::colorize('normal'); sleep(1); }; } else { echo 'Installation of sabre/' . "\n" . Welcome::LOGO, "\n\n", static::getBaseURLInfo(), "\n\n"; $step = function ($index, $label, callable $validator, $errorMessage, $default = '') use(&$readline) { do { echo $label; if (!empty($default)) { echo ' [default: ', $default, ']'; } $out = $readline->readLine(': '); if (empty($out)) { $out = $default; } $valid = $validator($out); if (true !== $valid) { echo $errorMessage, "\n"; } } while (true !== $valid); return $out; }; $progress = function ($percent, $message) { echo $message, "\n"; }; } $form['baseUrl'] = $step(0, 'Choose the base URL', function ($baseUrl) use($verbose) { $valid = Installer::checkBaseUrl($baseUrl); if (true === $valid && true === $verbose) { Cursor::move('↓'); } return $valid; }, 'Base URL must start and end by a slash' . "\n" . 'Check the Section “The base URL” on http://sabre.io/dav/gettingstarted/.', '/'); if (false === $verbose) { echo 'Your administrator login: '******'password'] = $step(1, 'Choose the administrator password', function ($administratorPassword) { return Installer::checkPassword($administratorPassword . $administratorPassword); }, 'Password must not be empty' . "\n" . 'An empty password is not a password anymore!'); $readline = $oldReadline; $form['email'] = $step(2, 'Choose the administrator email', function ($administratorEmail) { return Installer::checkEmail($administratorEmail . $administratorEmail); }, 'Email is invalid' . "\n" . 'The given email seems invalid.'); $databaseDriver =& $form['database']['driver']; if (true === $verbose) { $radioReadline = new Console\Readline\Password(); $radioReadline->addMapping('\\e[D', function () use($labelMaxWidth, &$databaseDriver) { $databaseDriver = 'sqlite'; Cursor::save(); Cursor::move('LEFT'); Cursor::move('→', $labelMaxWidth); Cursor::clear('→'); echo '🔘 SQLite ⚪️ MySQL'; Cursor::restore(); }); $radioReadline->addMapping('\\e[C', function () use($labelMaxWidth, &$databaseDriver) { $databaseDriver = 'mysql'; Cursor::save(); Cursor::move('LEFT'); Cursor::move('→', $labelMaxWidth); Cursor::clear('→'); echo '⚪️ SQLite 🔘 MySQL'; Cursor::restore(); }); Cursor::hide(); $radioReadline->readLine(); Cursor::show(); unset($databaseDriver); if ('mysql' === $form['database']['driver']) { echo 'Choose MySQL host: ', $input(), "\n", 'Choose MySQL port: ', $input('3306'), "\n", 'Choose MySQL username: '******'Choose MySQL password: '******'Choose MySQL database name: ', $input(), "\n"; Window::scroll('↑', 10); Cursor::move('↑', 10); $numberOfSteps = 5; Cursor::move('↑', $numberOfSteps); Cursor::move('→', $labelMaxWidth); Cursor::colorize('foreground(black) background(#cccccc)'); } } else { $form['database']['driver'] = $step(3, 'Choose the database driver (sqlite or mysql)', function ($databaseDriver) { return in_array($databaseDriver, ['sqlite', 'mysql']); }, 'Database driver is invalid' . "\n" . 'Database driver must be `sqlite` or `mysql`', 'sqlite'); } if ('mysql' === $form['database']['driver']) { $form['database']['host'] = $step(0, 'Choose MySQL host', function () { return true; }, ''); $form['database']['port'] = $step(1, 'Choose MySQL port', function ($port) { return false !== filter_var($port, FILTER_VALIDATE_INT); }, 'Port is invalid' . "\n" . 'Port must be an integer.', '3306'); $form['database']['username'] = $step(2, 'Choose MySQL username', function () { return true; }, ''); $oldReadline = $readline; $readline = new Console\Readline\Password(); $form['database']['password'] = $step(3, 'Choose MySQL password', function () { return true; }, ''); $readline = $oldReadline; $form['database']['name'] = $step(3, 'Choose MySQL database name', function () { return true; }, ''); } $readline->readLine("\n" . 'Ready to install? (Enter to continue, Ctrl-C to abort)'); echo "\n\n"; try { $progress(5, 'Create configuration file…'); $configuration = Installer::createConfigurationFile(Server::CONFIGURATION_FILE, ['baseUrl' => $form['baseUrl'], 'database' => $form['database']]); $progress(25, 'Configuration file created 👍!'); $progress(30, 'Create the database…'); $database = Installer::createDatabase($configuration); $progress(50, 'Database created 👍!'); $progress(55, 'Create administrator profile…'); Installer::createAdministratorProfile($configuration, $database, $form['email'], $form['password']); $progress(75, 'Administrator profile created 👍!'); $progress(100, 'sabre/katana is ready!'); } catch (\Exception $e) { $progress(-1, 'An error occured: ' . $e->getMessage()); if (null !== ($previous = $e->getPrevious())) { echo 'Underlying error: ' . $previous->getMessage(); } echo "\n", 'You are probably likely to run: ' . '`make uninstall` before trying again.', "\n"; return 2; } list($dirname) = Uri\split($form['baseUrl']); echo "\n\n", 'The administration interface will be found at this path: ', '<your website>', $dirname, '/admin.php.', "\n"; Window::setTitle($oldTitle); }
/** * Triggered before a node is deleted * * This allows us to check permissions for any operation that will delete * an existing node. * * @param string $uri * @return void */ function beforeUnbind($uri) { list($parentUri) = Uri\split($uri); $this->checkPrivileges($parentUri, '{DAV:}unbind', self::R_RECURSIVEPARENTS); }
/** * Triggered by a `DELETE`, `COPY` or `MOVE`. The goal is to remove the * home directory of the principal. * * @param string $path Path. * @return bool */ function afterUnbind($path) { list($collection, $principalName) = SabreUri\split($path); if ('principals' !== $collection) { return false; } $out = true; $path = $this->storagePath . DS . $principalName; if (is_dir($path)) { $directory = new HoaFile\Directory($this->storagePath . DS . $principalName); $out = $directory->delete(); $directory->close(); } return $out; }
/** * Checks if the submitted iCalendar data is in fact, valid. * * An exception is thrown if it's not. * * @param resource|string $data * @param string $path * @param bool $modified Should be set to true, if this event handler * changed &$data. * @param RequestInterface $request The http request. * @param ResponseInterface $response The http response. * @param bool $isNew Is the item a new one, or an update. * @return void */ protected function validateICalendar(&$data, $path, &$modified, RequestInterface $request, ResponseInterface $response, $isNew) { // If it's a stream, we convert it to a string first. if (is_resource($data)) { $data = stream_get_contents($data); } $before = $data; try { // If the data starts with a [, we can reasonably assume we're dealing // with a jCal object. if (substr($data, 0, 1) === '[') { $vobj = VObject\Reader::readJson($data); // Converting $data back to iCalendar, as that's what we // technically support everywhere. $data = $vobj->serialize(); $modified = true; } else { $vobj = VObject\Reader::read($data); } } catch (VObject\ParseException $e) { throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid iCalendar 2.0 data. Parse error: ' . $e->getMessage()); } if ($vobj->name !== 'VCALENDAR') { throw new DAV\Exception\UnsupportedMediaType('This collection can only support iCalendar objects.'); } $sCCS = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'; // Get the Supported Components for the target calendar list($parentPath) = Uri\split($path); $calendarProperties = $this->server->getProperties($parentPath, [$sCCS]); if (isset($calendarProperties[$sCCS])) { $supportedComponents = $calendarProperties[$sCCS]->getValue(); } else { $supportedComponents = ['VJOURNAL', 'VTODO', 'VEVENT']; } $foundType = null; foreach ($vobj->getComponents() as $component) { switch ($component->name) { case 'VTIMEZONE': continue 2; case 'VEVENT': case 'VTODO': case 'VJOURNAL': $foundType = $component->name; break; } } if (!$foundType || !in_array($foundType, $supportedComponents)) { throw new Exception\InvalidComponentType('iCalendar objects must at least have a component of type ' . implode(', ', $supportedComponents)); } $options = VObject\Node::PROFILE_CALDAV; $prefer = $this->server->getHTTPPrefer(); if ($prefer['handling'] !== 'strict') { $options |= VObject\Node::REPAIR; } $messages = $vobj->validate($options); $highestLevel = 0; $warningMessage = null; // $messages contains a list of problems with the vcard, along with // their severity. foreach ($messages as $message) { if ($message['level'] > $highestLevel) { // Recording the highest reported error level. $highestLevel = $message['level']; $warningMessage = $message['message']; } switch ($message['level']) { case 1: // Level 1 means that there was a problem, but it was repaired. $modified = true; break; case 2: // Level 2 means a warning, but not critical break; case 3: // Level 3 means a critical error throw new DAV\Exception\UnsupportedMediaType('Validation error in iCalendar: ' . $message['message']); } } if ($warningMessage) { $response->setHeader('X-Sabre-Ew-Gross', 'iCalendar validation warning: ' . $warningMessage); } // We use an extra variable to allow event handles to tell us wether // the object was modified or not. // // This helps us determine if we need to re-serialize the object. $subModified = false; $this->server->emit('calendarObjectChange', [$request, $response, $vobj, $parentPath, &$subModified, $isNew]); if ($modified || $subModified) { // An event handler told us that it modified the object. $data = $vobj->serialize(); // Using md5 to figure out if there was an *actual* change. if (!$modified && strcmp($data, $before) !== 0) { $modified = true; } } // Destroy circular references so PHP will garbage collect the object. $vobj->destroy(); }