/** * Post a new announcement * * @param array $post POST data from the Landing * @return bool */ public function createAnnouncement(array $post) : bool { $this->db->beginTransaction(); // We want a unique ID (collision chance 50% at 2^132) $query = 'SELECT count(*) FROM bridge_announcements WHERE uniqueid = ?'; do { $unique = \Airship\uniqueId(33); } while ($this->db->exists($query, $unique)); $this->db->insert('bridge_announcements', ['uniqueid' => $unique, 'title' => $post['title'] ?? '', 'contents' => $post['contents'] ?? '', 'format' => $post['format'] ?? 'HTML', 'only_admins' => !empty($post['only_admins'])]); return $this->db->commit(); }
/** * Update a blog post * * @param array $post * @param array $old * @param bool $publish * @return bool */ public function updatePost(array $post, array $old, bool $publish = false) : bool { $this->db->beginTransaction(); $postUpdates = []; // First, update the hull_blog_posts entry if (!empty($post['author'])) { if ($post['author'] !== $old['author']) { $postUpdates['author'] = (int) $post['author']; } } if ($post['description'] !== $old['description']) { $postUpdates['description'] = (string) $post['description']; } if ($post['format'] !== $old['format']) { $postUpdates['format'] = (string) $post['format']; } if ($post['slug'] !== $old['slug']) { $bm = (string) $old['blogmonth'] < 10 ? '0' . $old['blogmonth'] : $old['blogmonth']; $exists = $this->db->cell('SELECT count(*) FROM view_hull_blog_list WHERE blogmonth = ? AND blogyear = ? AND slug = ?', $old['blogyear'], $bm, $post['slug']); if ($exists > 0) { // Slug collision return false; } $postUpdates['slug'] = (string) $post['slug']; if (!empty($post['redirect_slug'])) { $oldUrl = \implode('/', ['blog', $old['blogyear'], $bm, $old['slug']]); $newUrl = \implode('/', ['blog', $old['blogyear'], $bm, $post['slug']]); $this->db->insert('airship_custom_redirect', ['oldpath' => $oldUrl, 'newpath' => $newUrl, 'cabin' => $this->cabin, 'same_cabin' => true]); } } $now = new \DateTime(); if (!empty($post['published'])) { try { $now = new \DateTime($post['published']); } catch (\Throwable $ex) { } } if (!\array_key_exists('category', $post)) { $post['category'] = 0; } if ($post['category'] !== $old['category']) { $postUpdates['category'] = (int) $post['category']; } if ($publish) { $postUpdates['status'] = true; $postUpdates['cache'] = !empty($post['cache']); // Let's set the publishing time. $postUpdates['published'] = $now->format(\AIRSHIP_DATE_FORMAT); } if ($post['title'] !== $old['title']) { $postUpdates['title'] = (string) $post['title']; } if (!empty($postUpdates)) { $this->db->update('hull_blog_posts', $postUpdates, ['postid' => $old['postid']]); } do { $unique = \Airship\uniqueId(); $exists = $this->db->exists('SELECT COUNT(*) FROM hull_blog_post_versions WHERE uniqueid = ?', $unique); } while ($exists); // Second, create a new entry in hull_blog_post_versions $this->db->insert('hull_blog_post_versions', ['post' => $old['postid'], 'body' => $post['blog_post_body'], 'format' => $post['format'], 'live' => $publish, 'metadata' => \json_encode($post['metadata'] ?? []), 'published_by' => $publish ? $this->getActiveUserId() : null, 'uniqueid' => $unique]); if (empty($old['tags'])) { $old['tags'] = []; } if (empty($post['tags'])) { $post['tags'] = []; } // Now let's update the tag relationships $tag_ins = \array_diff($post['tags'], $old['tags']); $tag_del = \array_diff($old['tags'], $post['tags']); foreach ($tag_del as $del) { $this->db->delete('hull_blog_post_tags', ['postid' => $old['postid'], 'tagid' => $del]); } foreach ($tag_ins as $ins) { $this->db->insert('hull_blog_post_tags', ['postid' => $old['postid'], 'tagid' => $ins]); } if ($publish) { \Airship\clear_cache(); } return $this->db->commit(); }
/** * ConfigFilter constructor. * * Specifies the filter rules for the cabin configuration POST rules. */ public function __construct() { $this->addFilter('config_extra.blog.cachelists', new BoolFilter())->addFilter('config_extra.blog.comments.depth_max', new IntFilter())->addFilter('config_extra.blog.comments.enabled', new BoolFilter())->addFilter('config_extra.blog.comments.guests', new BoolFilter())->addFilter('config_extra.blog.comments.recaptcha', new BoolFilter())->addFilter('config_extra.blog.per_page', new IntFilter())->addFilter('config_extra.file.cache', new IntFilter())->addFilter('config_extra.homepage.blog-posts', (new IntFilter())->setDefault(5))->addFilter('config_extra.cache-secret', (new StringFilter())->setDefault(\Airship\uniqueId(33)))->addFilter('config_extra.recaptcha.secret-key', new StringFilter())->addFilter('config_extra.recaptcha.site-key', new StringFilter())->addFilter('twig_vars.active-motif', new StringFilter())->addFilter('twig_vars.title', new StringFilter())->addFilter('twig_vars.tagline', new StringFilter())->addFilter('twig_vars.blog.title', new StringFilter())->addFilter('twig_vars.blog.tagline', new StringFilter()); }
/** * Get a unique ID (and make sure it doesn't exist) * * @param string $table * @param string $column * @return string */ protected function uniqueId(string $table, string $column = 'uniqueid') : string { do { $unique = \Airship\uniqueId(); } while ($this->db->exists('SELECT count(*) FROM ' . $this->db->escapeIdentifier($table) . ' WHERE ' . $this->db->escapeIdentifier($column) . ' = ?', $unique)); return $unique; }
<?php declare (strict_types=1); /** * This script runs when upgrading to v1.4.0 from an earlier version. * It adds the uniqueid column to the hull_blog_post_versions table then * retcons a uniqueid for each existing blog post version. */ $db = \Airship\get_database(); $db->exec('ALTER TABLE hull_blog_post_versions ADD uniqueid TEXT;'); foreach ($db->run('SELECT * FROM hull_blog_post_versions') as $ver) { // Get a unique ID: do { $unique = \Airship\uniqueId(); $exists = $db->exists('SELECT count(*) FROM hull_blog_post_versions WHERE uniqueid = ?', $unique); } while ($exists); // Now assign it. $db->update('hull_blog_post_versions', ['uniqueid' => $unique], ['versionid' => $ver['versionid']]); } // Finally... $db->exec('CREATE UNIQUE INDEX ON hull_blog_post_versions(uniqueid);');
/** * @covers \Airship\uniqueId() */ public function testUniqueId() { $strings = []; for ($i = 0; $i < 1024; ++$i) { $strings[] = \Airship\uniqueId(33); } $this->assertSame($strings, \array_unique($strings), 'Collision!'); for ($i = 18; $i < 30; ++$i) { $unique = \trim(\Airship\uniqueId($i), '='); $this->assertSame($i, Binary::safeStrlen($unique)); } }
<?php declare (strict_types=1); require_once \dirname(__DIR__) . '/src/bootstrap.php'; /** * This gives each user a uniqueid if they do not already have one. * * It *should* be safe to remove, but we're holding off on doing that * until version 2.0.0. */ $db = \Airship\get_database(); foreach ($db->run('SELECT * FROM airship_users WHERE uniqueid IS NULL OR LENGTH(uniqueid) < 24') as $r) { $db->update('airship_users', ['uniqueid' => \Airship\uniqueId()], ['userid' => $r['userid']]); echo "{$r['userid']}\n"; }
if ($state->universal['email']['smtp']['connection_class'] !== 'smtp') { $transportConfig['connection_config'] = ['username' => $state->universal['email']['smtp']['username'], 'password' => $state->universal['email']['smtp']['password']]; } if (!empty($state->universal['email']['smtp']['disable_tls'])) { $transportConfig['connection_config']['port'] = !empty($state->universal['email']['smtp']['port']) ? $state->universal['email']['smtp']['port'] : 25; } else { $transportConfig['connection_config']['ssl'] = 'tls'; $transportConfig['port'] = !empty($state->universal['email']['smtp']['port']) ? $state->universal['email']['smtp']['port'] : 587; } $transport->setOptions(new \Zend\Mail\Transport\SmtpOptions($transportConfig)); break; case 'File': $transport = new Zend\Mail\Transport\File(); /** @noinspection PhpUnusedParameterInspection */ $transport->setOptions(new \Zend\Mail\Transport\FileOptions(['path' => !empty($state->universal['email']['file']['path']) ? $state->universal['email']['file']['path'] : ROOT . '/files/email', 'callback' => function (Zend\Mail\Transport\File $t) : string { return \implode('_', ['Message', \date('YmdHis'), \Airship\uniqueId(12) . '.txt']); }])); break; case 'Sendmail': if (!empty($state->universal['email']['sendmail']['parameters'])) { $transport = new Zend\Mail\Transport\Sendmail($state->universal['email']['sendmail']['parameters']); } else { $transport = new Zend\Mail\Transport\Sendmail(); } break; default: throw new Exception(\trk('errors.email.invalid_transport', \print_r($state->universal['email']['transport'], true))); } } $state->mailer = $transport; }
/** * Generate a unique random public ID for this user, which is distinct from the username they use to log in. * * @return string */ protected function generateUniqueId() : string { $unique = ''; $query = 'SELECT count(*) FROM airship_users WHERE uniqueid = ?'; do { if (!empty($unique)) { // This will probably never be executed. It will be a nice easter egg if it ever does. $state = State::instance(); $state->logger->log(LogLevel::ALERT, "A unique user ID collision occurred. This should never happen. (There are 2^192 possible values," . "which has approximately a 50% chance of a single collision occurring after 2^96 users," . "and the database can only hold 2^64). This means you're either extremely lucky or your CSPRNG " . "is broken. We hope it's luck. Airship is clever enough to try again and not fail " . "(so don't worry), but we wanted to make sure you were aware.", ['colliding_random_id' => $unique]); } $unique = \Airship\uniqueId(); } while ($this->db->exists($query, $unique)); return $unique; }
/** * Create the default pages (about, contact). */ protected function finalDefaultPages() { foreach (\Airship\list_all_files(ROOT . '/Installer/default_pages') as $file) { $filedata = \file_get_contents($file); if (\preg_match('#/([^./]+).md$#', $file, $m)) { $pageid = $this->db->insertGet('airship_custom_page', ['cabin' => 'Hull', 'url' => $m[1], 'active' => true, 'cache' => false], 'pageid'); $this->db->insert('airship_custom_page_version', ['page' => $pageid, 'uniqueid' => \Airship\uniqueId(), 'published' => true, 'formatting' => 'Markdown', 'bridge_user' => 1, 'body' => $filedata, 'metadata' => '[]', 'raw' => false]); } } }