public function onEvent(GenericEvent $event, $eventName, $type, $user) { $ip = $this->request->getClientIp(); $userAgent = $this->request->getUserAgent(); $doc = ['uid' => (int) $user['uid'], 'at' => new \MongoDate(), 'type' => (int) $type, 'ua' => $userAgent, 'ip' => $ip]; Application::coll('LoginLog')->insert($doc); }
/** * 加入域 * * @param int $uid * @param \MongoId $domainId * @return bool * @throws InvalidArgumentException * @throws UserException */ public function joinDomainById($uid, \MongoId $domainId) { if (!Validator::int()->validate($uid)) { throw new InvalidArgumentException('uid', 'type_invalid'); } $uid = (int) $uid; if (!DomainUtil::isGlobalDomainId($domainId)) { // 检查域是否存在 $d = Application::coll('Domain')->findOne(['_id' => $domainId]); if (!DomainUtil::isDomainObjectValid($d)) { throw new UserException('DomainManager.joinDomain.invalid_domain'); } } // 检查用户是否存在 $user = UserUtil::getUserObjectByUid($uid); if (!UserUtil::isUserObjectValid($user)) { throw new UserException('DomainManager.joinDomain.invalid_user'); } // 添加 MEMBER 角色 Application::coll('UserRole')->update(['uid' => $uid], ['$addToSet' => ['d.' . (string) $domainId => 'DOMAIN_MEMBER']], ['upsert' => true]); // 创建空资料 $document = ['pref' => new \stdClass(), 'rp' => 0.0, 'rp_s' => 0.0, 'rank' => -1, 'level' => 0]; if (DomainUtil::isGlobalDomainId($domainId)) { $document += ['sig' => '', 'sigraw' => '', 'contacts' => []]; } Application::coll('UserInfo')->update(['uid' => $uid, 'domain' => new \MongoId(VJ::DOMAIN_GLOBAL)], ['$setOnInsert' => $document], ['upsert' => true]); // 操作非全局域则插入操作记录 if (!DomainUtil::isGlobalDomainId($domainId)) { $doc = ['uid' => $this->session->getCurrentToken(), 'at' => new \MongoDate(), 'type' => 'join', 'ua' => $this->request->getUserAgent(), 'ip' => $this->request->getClientIp(), 'target_uid' => $uid, 'target_domain' => $domainId]; Application::coll('DomainLog')->insert($doc); } return true; }
/** * @param string $username * @return array|null */ public static function getUserObjectByUsername($username) { if (!is_string($username) || !mb_check_encoding($username, 'UTF-8')) { return null; } $user = Application::coll('User')->findOne(['luser' => self::canonicalizeUsername($username)]); return $user; }
/** * 获取用户所在域 * * @param $uid * @return \MongoId[]|null * @throws InvalidArgumentException */ public static function getUserDomains($uid) { if (!Validator::int()->validate($uid)) { throw new InvalidArgumentException('uid', 'type_invalid'); } $record = Application::coll('UserRole')->findOne(['uid' => (int) $uid]); // user not found if ($record === null) { return null; } $domainIds = array_keys($record['d']); return array_map(function ($id) { return new \MongoId($id); }, $domainIds); }
private function importKeyword(OutputInterface $output, $file, $field) { $output->writeln('Importing keyword ' . $file . '...'); $file = base64_decode(file_get_contents(__DIR__ . '/data/' . $file)); $keywords = []; $lines = explode("\n", $file); foreach ($lines as $line) { if (strlen($line) > 0) { $keywords[] = trim(mb_strtolower($line, 'UTF-8')); } } $keywords = array_values(array_unique($keywords)); Application::coll('System')->update(['_id' => 'FilterKeyword'], ['$set' => [strval($field) => $keywords]], ['upsert' => true]); $output->writeln('<info>Imported ' . count($keywords) . ' keywords.</info>'); }
public function testAppendLog() { $cases = [['type' => VJ::LOGIN_TYPE_FAILED_WRONG_PASSWORD, 'event' => 'user.login.failed.wrong_password'], ['type' => VJ::LOGIN_TYPE_INTERACTIVE, 'event' => 'user.login.succeeded'], ['type' => VJ::LOGIN_TYPE_TOKEN, 'event' => 'user.login.succeeded']]; foreach ($cases as $case) { $request = new Request([], [], [], [], [], ['PHP_SELF' => '/app.php', 'REQUEST_METHOD' => 'GET', 'HTTP_USER_AGENT' => 'chrome', 'REMOTE_ADDR' => '1.2.3.4', 'SERVER_PORT' => 80]); Application::coll('LoginLog')->remove(); $service = new LoginLogService($request); $service->onEvent(new GenericEvent(), $case['event'], $case['type'], ['uid' => 123]); $this->assertEquals(1, Application::coll('LoginLog')->find()->count()); $rec = Application::coll('LoginLog')->findOne(); $this->assertNotNull($rec); $this->assertEquals(123, $rec['uid']); $this->assertEquals($case['type'], $rec['type']); $this->assertEquals('chrome', $rec['ua']); $this->assertEquals('1.2.3.4', $rec['ip']); $this->assertLessThanOrEqual(5, time() - $rec['at']->sec); $this->assertGreaterThanOrEqual(-2, time() - $rec['at']->sec); } }
/** * 导入 Vijos 数据库用户表 * * @param callable $progress * @param callable $done * @return bool * @throws UserException */ public function import(callable $progress = null) { if (Application::coll('User')->count() > 0) { throw new UserException('User.Importer.VijosImporter.userCollectionNotEmpty'); } $MAX_USER_ID = 1; $cursor = $this->source->selectCollection('User')->find()->sort(['_id' => 1]); foreach ($cursor as $user) { $_id = new \MongoId(); $uid = (int) $user['_id']; if ($uid > $MAX_USER_ID) { $MAX_USER_ID = $uid; } // vijos 中,username 被 escape 过 $user['user'] = htmlspecialchars_decode($user['user']); if (is_callable($progress)) { $progress($uid, $user['user']); } $doc = ['_id' => $_id, 'uid' => $uid, 'user' => $user['user'], 'luser' => UserUtil::canonicalizeUsername($user['user']), 'g' => $user['g'], 'gender' => (int) $user['sex'] + 1, 'regat' => (int) $user['treg'], 'regip' => $user['ipreg'], 'loginat' => (int) $user['tlogin'], 'loginip' => '255.255.255.255']; // 检查是否有 Email 重叠 $count = $this->source->selectCollection('User')->find(['lmail' => mb_strtolower($user['mail'], 'UTF-8')])->count(); if ($count > 1) { $doc['mail'] = Application::get('random')->generateString(20, VJ::RANDOM_CHARS) . '@openvj'; $doc['lmail'] = mb_strtolower($doc['mail'], 'UTF-8'); $doc['omail'] = $user['mail']; } else { $doc['mail'] = $user['mail']; $doc['lmail'] = UserUtil::canonicalizeEmail($doc['mail']); $doc['salt'] = $user['salt']; $doc['hash'] = 'vj2|' . base64_encode(mb_strtolower($user['user'], 'UTF-8')) . '|' . $user['pass']; } try { Application::coll('User')->insert($doc); Application::emit('user.created', [$uid]); } catch (\MongoCursorException $e) { // TODO: Duplicate user } } // Update UID counter Application::coll('System')->update(['_id' => 'UserCounter'], ['$set' => ['count' => $MAX_USER_ID + 1]], ['upsert' => true]); return true; }
protected function execute(InputInterface $input, OutputInterface $output) { $helper = $this->getHelper('question'); if ($input->getOption('type') !== 'vijos') { $output->writeln('<error>Only support --type=vijos</error>'); return; } if (Application::coll('User')->count() > 0) { $collectionToDrop = ['User', 'UserRole', 'UserInfo']; $q = new ConfirmationQuestion("User collection exists. Drop collections and continue? [y/n]", false); if ($helper->ask($input, $output, $q)) { foreach ($collectionToDrop as $collectionName) { Application::coll($collectionName)->remove(); $output->writeln('<info>Dropped ' . $collectionName . '.</info>'); } } else { return; } } $output->writeln('Testing connection to the data source...'); try { $client = new \MongoClient(); $db = $client->selectDB('vijos'); $db->selectCollection('User')->count(); } catch (\Exception $e) { $output->writeln('<error>Connect failed :' . $e->getMessage() . '</error>'); return; } $count = 0; $output->writeln('Begin importing...'); $importer = new VijosImporter($db); $importer->import(function ($uid, $username) use(&$output, &$count) { $output->writeln('Importing #' . $uid . ' ' . $username); $count++; }); $output->writeln('<info>Imported ' . $count . ' users.</info>'); }
protected function execute(InputInterface $input, OutputInterface $output) { $output->writeln('Rebuilding indexes...'); Application::coll('System')->deleteIndexes(); Application::coll('User')->deleteIndexes(); Application::coll('User')->ensureIndex(['uid' => 1], ['unique' => true]); Application::coll('User')->ensureIndex(['luser' => 1], ['unique' => true]); Application::coll('User')->ensureIndex(['lmail' => 1], ['unique' => true]); Application::coll('UserRole')->deleteIndexes(); Application::coll('UserRole')->ensureIndex(['uid' => 1], ['unique' => true]); Application::coll('PermissionAllow')->deleteIndexes(); Application::coll('PermissionAllow')->ensureIndex(['domain' => 1, 'val' => 1, 'role' => 1]); Application::coll('Role')->deleteIndexes(); Application::coll('Role')->ensureIndex(['domain' => 1, 'name' => 1], ['unique' => true]); Application::coll('Role')->ensureIndex(['internal' => 1, 'name' => 1], ['unique' => true]); Application::coll('UserInfo')->deleteIndexes(); Application::coll('UserInfo')->ensureIndex(['uid' => 1, 'domain' => 1], ['unique' => true]); Application::coll('Domain')->deleteIndexes(); Application::coll('DomainLog')->deleteIndexes(); Application::coll('Token')->deleteIndexes(); Application::coll('Token')->ensureIndex(['purpose' => 1, 'identifier' => 1], ['unique' => true]); Application::coll('Token')->ensureIndex(['purpose' => 1, 'token' => 1]); Application::coll('Token')->ensureIndex(['expireat' => 1], ['expireAfterSeconds' => 0]); Application::coll('Session')->deleteIndexes(); Application::coll('Session')->ensureIndex(['expireat' => 1], ['expireAfterSeconds' => 0]); Application::coll('RememberMeToken')->deleteIndexes(); Application::coll('RememberMeToken')->ensureIndex(['uid' => 1, 'token' => 1]); Application::coll('RememberMeToken')->ensureIndex(['expireat' => 1], ['expireAfterSeconds' => 0]); Application::coll('LoginLog')->deleteIndexes(); Application::coll('LoginLog')->ensureIndex(['uid' => 1, 'at' => -1]); Application::coll('Comment')->deleteIndexes(); Application::coll('Comment')->ensureIndex(['ref' => 1, 'deleted' => 1, '_id' => 1]); Application::coll('Problem')->deleteIndexes(); Application::coll('Problem')->ensureIndex(['owner' => 1, 'llink' => 1], ['unique' => true]); $output->writeln('<info>Done.</info>'); }
/** * 使 token 无效 * * @param string $purpose * @param string $token * @return bool * @throws InvalidArgumentException */ public function invalidate($purpose, $token) { if (!is_string($purpose)) { throw new InvalidArgumentException('purpose', 'type_invalid'); } if (!mb_check_encoding($purpose, 'UTF-8')) { throw new InvalidArgumentException('purpose', 'encoding_invalid'); } if (!is_string($token)) { throw new InvalidArgumentException('token', 'type_invalid'); } if (!mb_check_encoding($token, 'UTF-8')) { throw new InvalidArgumentException('token', 'encoding_invalid'); } $result = Application::coll('Token')->remove(['purpose' => $purpose, 'token' => $token], ['justOne' => true]); return $result['n'] === 1; }
/** * 是否拥有一项权限(白名单) * * @param string $permissionName * @param \MongoId $domainId * @param bool $isResourceOwner * @return bool */ public function hasPermission($permissionName, \MongoId $domainId, $isResourceOwner = false) { $roles = $this->getRoles($domainId, $isResourceOwner); $result = Application::coll('PermissionAllow')->findOne(['domain' => $domainId, 'val' => $permissionName, 'role' => ['$in' => $roles]]); return $result !== null; }
/** * 创建用户 * * @param string $username * @param string $password * @param string $email * @return int UID * @throws InvalidArgumentException * @throws UserException */ public function createUser($username, $password, $email) { if (!is_string($username)) { throw new InvalidArgumentException('username', 'type_invalid'); } if (!is_string($password)) { throw new InvalidArgumentException('password', 'type_invalid'); } if (!is_string($email)) { throw new InvalidArgumentException('email', 'type_invalid'); } // 检查用户名 if (!mb_check_encoding($username, 'UTF-8')) { throw new InvalidArgumentException('username', 'encoding_invalid'); } $username = trim($username); if (!Validator::regex('/^\\S*$/')->length(3, 16)->validate($username)) { throw new InvalidArgumentException('username', 'format_invalid'); } // 检查关键字 $keyword = KeywordFilter::isContainGeneric($username); if ($keyword === false) { $keyword = KeywordFilter::isContainName($username); } if ($keyword !== false) { throw new UserException('UserManager.name_forbid', ['keyword' => $keyword]); } // 检查密码 if (!Validator::length(0, 50)->validate($password)) { throw new InvalidArgumentException('password', 'format_invalid'); } // 检查 email if (!Validator::email()->validate($email)) { throw new InvalidArgumentException('password', 'format_invalid'); } // 处理用户名 $username = VJ::removeEmoji($username); // 检查用户名和 Email 是否唯一 if (UserUtil::getUserObjectByUsername($username) !== null) { throw new UserException('UserManager.createUser.user_exists'); } if (UserUtil::getUserObjectByEmail($email) !== null) { throw new UserException('UserManager.createUser.email_exists'); } // 生成 hash & salt $hashSaltPair = $this->user_credential->password_encoder->generateHash($password); // 插入记录 try { $_id = new \MongoId(); $doc = ['_id' => $_id, 'uid' => $_id, 'user' => $username, 'luser' => UserUtil::canonicalizeUsername($username), 'mail' => $email, 'lmail' => UserUtil::canonicalizeEmail($email), 'salt' => $hashSaltPair['salt'], 'hash' => $hashSaltPair['hash'], 'g' => $email, 'gender' => VJ::USER_GENDER_UNKNOWN, 'regat' => new \MongoDate(), 'regip' => $this->request->getClientIp()]; Application::coll('User')->insert($doc); } catch (\MongoCursorException $e) { // 插入失败 throw new UserException('UserManager.createUser.user_or_email_exists'); } // 插入成功:更新 uid // 获取递增 uid $counterRec = Application::coll('System')->findAndModify(['_id' => 'UserCounter'], ['$inc' => ['count' => 1]], [], ['new' => true, 'upsert' => true]); $uid = (int) $counterRec['count']; try { // 修改 uid Application::coll('User')->update(['_id' => $_id], ['$set' => ['uid' => $uid]]); } catch (\MongoCursorException $e) { // 修改 uid 失败(uid 重复),则删除用户记录 Application::critical('createUser.uidDuplicate', ['uid' => $uid]); Application::coll('User')->remove(['_id' => $_id], ['justOne' => true]); throw new UserException('UserManager.createUser.internal'); } Application::emit('user.created', [$uid]); return $uid; }
public function testInvalidateRememberMeClientToken() { $token = Application::get('user_credential')->createRememberMeToken(0, '1.2.3.4', "VJTest/233", time() + 60); Application::get('user_credential')->invalidateRememberMeToken($token); $throw = false; try { Application::get('user_credential')->checkRememberMeTokenCredential($token, true); } catch (UserException $e) { $throw = true; $this->assertEquals('UserCredential.checkRememberMeTokenCredential.invalid_rememberme_token', $e->getUserErrorCode()); } $this->assertTrue($throw, 'Expect thrown exception'); $record = Application::coll('Token')->findOne(['purpose' => 'rememberme', 'token' => $token]); $this->assertNull($record); }
/** * 获得一个域下所有角色 * * @param \MongoId $domain * @return array */ public static function getRolesByDomainId(\MongoId $domain) { $cursor = Application::coll('Role')->find(['$or' => [['internal' => true], ['domain' => $domain]]]); return iterator_to_array($cursor, false); }
private static function initSession() { self::set('session_storage', function () { return new NativeSessionStorage(['name' => self::get('config')['session']['name'], 'cookie_httponly' => true], new MongoDBSessionHandler(Application::coll('Session'), (int) self::get('config')['session']['ttl'])); }); }
/** * 修改主题 * * @param \MongoId $discussionId * @param string $markdown * @return array|null * @throws InvalidArgumentException * @throws UserException */ public static function modifyDiscussion(\MongoId $discussionId, $markdown) { if (!is_string($markdown)) { throw new InvalidArgumentException('markdown', 'type_invalid'); } if (!mb_check_encoding($markdown, 'UTF-8')) { throw new InvalidArgumentException('markdown', 'encoding_invalid'); } //if (!Validator::length(VJ::COMMENT_MIN, VJ::COMMENT_MAX)) { //throw new UserException('CommentUtil.content_invalid_length'); //} self::initParser(); $html = self::$parser->parse($markdown); $keyword = KeywordFilter::isContainGeneric(strip_tags($html)); if ($keyword !== false) { throw new UserException('CommentUtil.content_forbid', ['keyword' => $keyword]); } $result = Application::coll('Discussion')->update(['_id' => $discussionId], ['$set' => ['raw' => $markdown, 'html' => $html]]); if ($result['n'] === 1) { Application::emit('discussion.modify.succeeded', [$discussionId]); return ['_id' => $discussionId, 'html' => $html]; } else { return null; } }
/** * 接收 Vijos Uid,返回其位于数据库中的 _id * * @param $vijosUid * @return array */ public function resolveUid($vijosUid) { $user = Application::coll('User')->find(['uid' => (int) $vijosUid]); return $user['_id']; }
/** * 删除回复 * * @param \MongoId $commentId * @param string $ref * @param \MongoId $replyId * @return bool * @throws InvalidArgumentException */ public static function deleteReply(\MongoId $commentId, $ref, \MongoId $replyId) { if (!is_string($ref)) { throw new InvalidArgumentException('ref', 'type_invalid'); } if (!mb_check_encoding($ref, 'UTF-8')) { throw new InvalidArgumentException('ref', 'encoding_invalid'); } $result = Application::coll('Comment')->update(['_id' => $commentId, 'ref' => $ref, 'deleted' => false, 'replies' => ['$elemMatch' => ['_id' => $replyId, 'deleted' => false]]], ['$set' => ['replies.$.deleted' => true]]); $success = $result['n'] === 1; if ($success) { Application::emit('comment.reply.delete.succeeded', [$ref, $commentId, $replyId]); } return $success; }
public static function modifyMeta(\MongoId $pid, $title, array $tags, array $visibleDomains) { if (!is_string($title)) { throw new InvalidArgumentException('title', 'type_invalid'); } if (!mb_check_encoding($title, 'UTF-8')) { throw new InvalidArgumentException('title', 'encoding_invalid'); } if (!Validator::length(VJ::PROBLEM_TITLE_MIN, VJ::PROBLEM_TITLE_MAX)) { throw new UserException('ProblemUtil.title_invalid_length'); } $keyword = KeywordFilter::isContainGeneric($title); if ($keyword !== false) { throw new UserException('ProblemUtil.title_forbid', ['keyword' => $keyword]); } foreach ($visibleDomains as $domain) { if (!$domain instanceof \MongoId) { throw new InvalidArgumentException('visibleDomains', 'type_invalid'); } } foreach ($tags as $tag) { if (!$tag instanceof Tag) { throw new InvalidArgumentException('tags', 'type_invalid'); } } $title = VJ::removeEmoji($title); $link = self::generateLink($title); try { $result = Application::coll('Problem')->update(['_id' => $pid], ['$set' => ['title' => $title, 'link' => $link, 'llink' => self::canonicalizeLink($link), 'tags' => array_map(function (Tag $tag) { return $tag->serializeForDb(); }, $tags), 'visible' => $visibleDomains]]); } catch (\MongoCursorException $e) { throw new UserException('ProblemUtil.modifyMeta.title_exists'); } return $result['n'] === 1; }
/** * 测试是否包含用于用户名的额外关键字 * * @param $text * @return bool|string */ public static function isContainName($text) { return Application::get('keyword_filter')->contains($text, 'name', function () { $rec = Application::coll('System')->findOne(['_id' => 'FilterKeyword']); if (!$rec) { return []; } else { return $rec['name']; } }); }
protected function query($chunk) { return Application::coll('Topic')->find(['_id' => ['$in' => $chunk]]); }
/** * 设置用户密码 * * @param int $uid * @param string $password * @return bool * @throws InvalidArgumentException */ public function setCredential($uid, $password) { if (!Validator::int()->validate($uid)) { throw new InvalidArgumentException('uid', 'type_invalid'); } $newHashSaltPair = $this->password_encoder->generateHash($password); $status = Application::coll('User')->update(['uid' => (int) $uid], ['$set' => ['hash' => $newHashSaltPair['hash'], 'salt' => $newHashSaltPair['salt']]]); if ($status['n'] === 1) { Application::info('credential.set', ['uid' => $uid]); return true; } else { return false; } }
/** * 获取分类 * * @param \MongoId $topicId * @return array|null */ public static function getTopicById(\MongoId $topicId) { $doc = Application::coll('Topic')->findOne(['_id' => $topicId]); return $doc; }