本教程是 Envato Tuts +上的“ 使用PHP构建启动”系列 的一部分。 在本系列文章中,我 将以 我的 Meeting Planner 应用程序作为真实示例 ,指导您完成从概念到现实的启动 。 在此过程的每一步中,我都会将Meeting Planner代码作为开放源代码示例发布,您可以从中学习。 我还将解决与启动相关的业务问题。
通过快捷方式安排小组会议
欢迎! 我最近从世界上最喜欢的地方回来, 上一集结尾提到了这个地方。 在完成多个参与者会议之后 ,我休息了一段时间。
今天,我将通过共享与您的会议关联的安全URL来添加邀请会议参与者的功能。 这对于安排小组会议特别有用。 例如,如果您想邀请30个人,有时将带有邀请URL的电子邮件发送给所有人更容易。
如果您还没有,请立即尝试安排自己的小组会议 ! 邀请几个朋友与您见面,品尝康普茶,卡瓦茶或咖啡。 在下面的评论中分享您对每个人的经历的想法和反馈。 我确实参与了讨论,但是您也可以在Twitter上与我联系@reifman 。 我总是乐于接受Meeting Planner的新功能建议以及有关未来系列剧集的建议。
提醒一下,Meeting Planner的所有代码都是开源提供的,并使用PHP的Yii2框架编写。 如果您想了解有关Yii2的更多信息,请查看我的平行系列“ 使用Yii2编程” 。
在深入探讨此功能之前,我想举例说明在构建服务时遇到的一些常见错误(或疏忽)。 (如果您只想阅读安全的共享URL,请随时跳过本节。)
启动错误插曲
随着越来越多的人开始尝试使用Meeting Planner,bug不断涌现。而且,在开发过程中,我常常会自己注意到它们。 以下是一些最近的一些,只是为了让您了解启动生活。
很难专注于业务开发和营销,编码新功能以及识别和修复错误。 随着您网站功能的增加,一人创业公司的难度也会增加。
如前所述, 我目前正在使用Asana进行功能规划,但同时也用于跟踪错误。
如果分配错误
我可以确定,如果我有更多的开发人员专业知识,与同事一起工作,或者有更多时间不编写会议计划程序代码,那么我将确切地知道哪些Atom编辑器扩展程序可以满足这些需求。 如果您知道,请在评论中发布。
显然,在检查查看者是否实际上是会议参加者的一项重要功能中,我正在使用作业进行检查。 换句话说,我不是在问所有者是否是查看者,而是在临时提出来。
public static function isAttendee($meeting_id,$user_id) {
$m = Meeting::findOne($meeting_id);
// are they the organizer?
// EEEK!
if ($m->owner_id = $user_id) {
return true;
}
您还记得,两个等于是一个比较,一个等于是一个赋值。 就像句号是串联的,加号是加号的一样,除了JavaScript之外,在JavaScript中它们无穷无尽地发现bug (这也是Ajax在PHP中是地狱的原因)。
随着时间的推移失败的数据库查询
随着我自己对Meeting Planner的使用的增加,我的选项卡式视图中有越来越多的会议。 然后我发现有时会出现重复。 当数据较少时,这很难早期发现。
我特定于选项卡的会议查询(例如,计划会议,确认的会议,过去的会议等)没有隔离唯一的条目:
$planningProvider = new ActiveDataProvider([
'query' => Meeting::find()->joinWith('participants')
->where(['owner_id'=>Yii::$app->user->getId()])
->orWhere(['participant_id'=>Yii::$app->user->getId()])
->andWhere(['meeting.status'=>[Meeting::STATUS_PLANNING,Meeting::STATUS_SENT]])
/* NEEDED TO ADD THIS */
->distinct(),
'sort'=> ['defaultOrder' => ['created_at'=>SORT_DESC]],
'pagination' => [
'pageSize' => 7,
'params' => array_merge($_GET, ['tab' => 'planning']),
],
]);
在查询中添加->distinct()
可修复该问题。
Yii2分页选项卡上的网格视图
我在遇到更多数据时遇到的另一个错误是Yii2分页链接始终使我回到第一个选项卡。
我添加了对当前选项卡,其MeetingController.php查询参数actionIndex
现在看起来为:
public function actionIndex()
{
if (Meeting::countUserMeetings(Yii::$app->user->getId())==0) {
$this->redirect(['create']);
}
$tab ='planning';
if (isset(Yii::$app->request->queryParams['tab'])) {
$tab =Yii::$app->request->queryParams['tab'];
}
$planningProvider = new ActiveDataProvider([
'query' => Meeting::find()->joinWith('participants')->where(['owner_id'=>Yii::$app->user->getId()])->orWhere(['participant_id'=>Yii::$app->user->getId()])->andWhere(['meeting.status'=>[Meeting::STATUS_PLANNING,Meeting::STATUS_SENT]])->distinct(),
'sort'=> ['defaultOrder' => ['created_at'=>SORT_DESC]],
'pagination' => [
'pageSize' => 7,
'params' => array_merge($_GET, ['tab' => 'planning']),
],
]);
另外,我指示pagination params
合并当前的选项卡设置。 当用户单击其他页面链接时,将包括当前选项卡。
最后,我还使/frontend/views/meeting/index.php意识到了这一点,并通过查询参数设置了活动标签:
<!-- Tab panes -->
<div class="tab-content">
<div class="tab-pane <?= ($tab=='planning'?'active':'') ?>" id="planning">
<div class="meeting-index">
<?php
?>
<?= $this->render('_grid', [
'mode'=>'planning',
'dataProvider' => $planningProvider,
'timezone'=>$timezone,
]) ?>
</div> <!-- end of planning meetings tab -->
</div>
<div class="tab-pane <?= ($tab=='upcoming'?'active':'') ?>" id="upcoming">
<div class="meeting-index">
<?= $this->render('_grid', [
'mode'=>'upcoming',
'dataProvider' => $upcomingProvider,
'timezone'=>$timezone,
]) ?>
</div> <!-- end of upcoming meetings tab -->
</div>
<div class="tab-pane <?= ($tab=='past'?'active':'') ?>" id="past">
<?= $this->render('_grid', [
'mode'=>'past',
'dataProvider' => $pastProvider,
'timezone'=>$timezone,
]) ?>
</div> <!-- end of past meetings tab -->
这些只是我遇到的日常错误的几个很好的例子,这些错误是我在Meeting Planner范围内构建启动公司时遇到的。
现在,让我们按承诺通过快捷方式URL来构建邀请。
构建安全的共享快捷方式URL
考虑URL的安全性
为了使随机发送的URL猜测难以破解某人的会议邀请,我需要一个相当独特的密钥以及一个无法猜测的代码。
我决定使用此人的用户名作为密钥。 每个用户都会有大量几乎无法猜测的会议代码。
因此,例如,会议URL可能是https://meetingplanner.io/presidenthillary/X1Y2Z3A7C9
。
对于代码,我决定使用八个区分大小写的字母数字字符。 换句话说,每个字符都是az,AZ或0-9,每个字符实质上有62种可能性。
每个用户的可能性总数为218,340,105,584,896-超过218万亿。 哦,您必须知道目标的用户名才能上手! 黑客入侵参与者的电子邮件帐户会容易得多。
为每次会议添加安全代码
要将安全代码添加到所有现有会议中,我创建了一个迁移m160902_174350_extend_meeting_for_identifier.php:
class m160902_174350_extend_meeting_for_identifier extends Migration
{
public function up()
{
$tableOptions = null;
if ($this->db->driverName === 'mysql') {
$tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB';
}
$this->addColumn('{{%meeting}}','identifier',Schema::TYPE_STRING.' NOT NULL');
$all = Meeting::find()
->where(['identifier'=>''])
->all();
foreach ($all as $m) {
$m->identifier = Yii::$app->security->generateRandomString(8);
$m->update();
}
}
您会注意到,在此迁移中,我实际上使用代码为每个现有会议创建随机字符串,即Yii::$app->security->generateRandomString(8);
。
我通常不会在迁移中编写代码来更新数据库的现有区域。 在这种情况下,它可以顺利运行。 其他时候,我使用了/frontend/models/Fix.php模型。
另外,在Meeting::beforeSave()
,我添加了自动代码来为以后的所有会议生成标识符:
public function beforeSave($insert)
{
if (parent::beforeSave($insert)) {
if ($insert) {
$this->identifier = Yii::$app->security->generateRandomString(8);
}
}
return true;
}
扩展Yii路由
包括控制器前缀(例如/m/username/identity-code
是最容易的,但我希望链接要简单,没有附加前缀。 这需要扩展Yii路由 。
如果我将其保留在自己的前缀和用户名模型中,那么我也许可以使用我在Yii2 Sluggable Behaviors and Building Your Startup:Geolocation和Google Places中写的内容。
相反,我添加了'<username>/<identity:[A-Za-z0-9_-]{8}>' => 'meeting/identity',
它将带有身份字符串的任何用户名映射到MeetingController actionIdentity()
方法。 。
'urlManager' => [
'class' => 'yii\web\UrlManager',
'enablePrettyUrl' => true,
'showScriptName' => false,
//'enableStrictParsing' => false,
'rules' => [
'place' => 'place',
'place/yours' => 'place/yours',
'place/create' => 'place/create',
'place/create_geo' => 'place/create_geo',
'place/create_place_google' => 'place/create_place_google',
'place/view/<id:\d+>' => 'place/view',
'place/update/<id:\d+>' => 'place/update',
'place/<slug>' => 'place/slug',
'<controller:\w+>/<id:\d+>' => '<controller>/view',
'<controller:\w+>/<action:\w+>/<id:\d+>' => '<controller>/<action>',
'daemon/<action>' => 'daemon/<action>', // incl eight char action
'site/<action>' => 'site/<action>', // incl eight char action
'features' => 'site/features',
'about' => 'site/about',
'<username>/<identity:[A-Za-z0-9_-]{8}>' => 'meeting/identity',
// note - currently actions with 8 letters and no params will fail
'<controller:\w+>/<action:\w+>' => '<controller>/<action>',
],
],
这样,我确实遇到了一些问题。 我必须对规则进行重新排序,并为可能看起来像用户名(而不是控制器)和方法(而不是标识密钥)的任何八个字符动作设置静态路由。
例如,将https://meetingplanner.io/site/features映射到名为“站点”的安全会议ID的用户站点,而不是Meeting Planner的新功能表。
但是,一旦我将自己定位为问题,一切都会正常进行。
会议控制器标识方法
接下来,我在MeetingController中构建了actionIdentity()
:
public function actionIdentity()
{
// fetch path
list($username,$identifier) = explode("/",Yii::$app->request->getPathInfo());
// verify the meeting identifier
$m = Meeting::find()
->where(['identifier'=>$identifier])
->one();
if (is_null($m) || ($m->owner->username != $username)) {
// access failure
return $this->redirect(['site/authfailure']);
}
// identifier is authentic
if (Yii::$app->user->isGuest) {
// redir to Participant join form
return $this->redirect(['/participant/join','meeting_id'=>$m->id,'identifier'=>$identifier]);
} else {
$user_id = Yii::$app->user->getId();
if (!Meeting::isAttendee($m->id,$user_id)) {
// if not an attendee -- add them as a participant
Participant::add($m->id,$user_id,$m->owner_id);
}
return $this->actionView($m->id);
}
首先,它验证用户名和身份对应于现有用户和现有会议。 如果不是,我们将它们发送给authfailure
。
如果用户已经登录,我们会自动将其作为参与者添加到此页面,并将其重定向到会议的查看页面。
如果不是,则将它们发送给参与者控制器的“加入”操作以解决登录或注册问题。
参加者要求参加会议
例如,假设我收到一个朋友的以下电子邮件邀请:
https://meetingplanner.io/tomeMcFarline/JzRq1a42
。 将显示此页面:
如果用户希望通过社交网络注册Meeting Planner,我们将能够验证其电子邮件地址。
因此,我设置了Yii返回URL(成功注册或登录后用户重定向到的页面),它将在身份验证后将其返回到“会议”视图页面。
// set return Url
Yii::$app->user->setReturnUrl($m->getSharingUrl());
在大多数情况下,社交身份验证,登录和/或注册由我在构建您的启动:使用OAuth简化Onramp中描述的代码进行管理。
如果参与者是新来的,他们将提供其名字和姓氏以及电子邮件地址。 我们会将其作为未经验证的参与者添加到会议中,类似于当用户通过向会议添加新的电子邮件地址来邀请某人时的操作。
$model = new Participant;
$model->meeting_id = $meeting_id;
...
$model->invited_by = $m->owner_id;
$model->status = Participant::STATUS_DEFAULT;
if (!$validationError && $model->validate()) {
$model->participant_id = User::addUserFromEmail($model->email);
$model->save();
// look up email to see if they exist
Meeting::displayNotificationHint($meeting_id);
$user = User::findOne($model->participant_id);
Yii::$app->user->login($user);
return $this->redirect(['/meeting/view', 'id' => $meeting_id]);
}
您现在想知道,嘿,杰夫,今天...
是什么? 这很简单,对吧? 我们只是将新用户添加到会议中。
在编写此代码时,我意识到自己正在制造一个巨大的隐私漏洞。
一个有趣的安全漏洞示例
假设汤姆·麦克法兰(Tom McFarlin)有一天无事可做,决定与我(和上帝)混在一起。 他将创建一个新的会议,并且知道达赖喇嘛是常规的会议计划者用户(由于他所有的属灵会议),因此McFarlin会使用电子邮件dalailama@gmail.com将他添加到他的会议中。
然后,他将获取他喜欢的安全快捷方式URL。 跟着我?
接下来,McFarlin将打开其他浏览器,并打开其安全快捷方式URL,并假装他是达赖喇嘛,他刚刚通过Tom收到了一封来自电子邮件的不同邀请,即他将尝试参加自己的会议,就好像他是达赖喇嘛一样。 即达赖,喇嘛,dalailama @ gmail.com。
最初,我认为没有人会猜到安全URL,因此,如果发生这种情况,我只会让一个人以这种方式登录。
但是,这会使危险的McFarlin能够访问达赖喇嘛的帐户(部分原因是我尚未建立通过URL进入的用户只能在一次登录后才能看到单个会议的受限访问模式)。
是的,我的初始代码以这种方式工作。 然后我接到了一个从天而降的电话,并指出了这一点。
如果McFarlin邀请Sally并且Sally将安全URL转发给Bill Gates怎么办? 通过首先手动将比尔·盖茨添加到会议中,麦克法兰可以使用此技巧访问盖茨的所有会议。
新代码要求参加者使用已经偶然添加到会议中的安全URL手动登录。 这是...
代码:
if ($model->load(Yii::$app->request->post())) {
// asking does the person joining already exist in User table
// might have been added to invitation by organizer or might already be a registered user
$person = User::find()->where(['email'=>$model->email])->one();
if (!is_null($person)) {
// user email already exists
// improve their profile
$postedVars = Yii::$app->request->post();
if (!empty($postedVars['Participant']['firstname'])) {
$model->firstname = $postedVars['Participant']['firstname'];
}
if (!empty($postedVars['Participant']['lastname'])) {
$model->lastname = $postedVars['Participant']['lastname'];
}
UserProfile::improve($person->id,$model->firstname,$model->lastname);
// are they an attendee
if (Meeting::isAttendee($model->meeting_id,$person->id)) {
/*
// to do note - this has to be changed to restricted access mode or removed
$identity = $person->findIdentity($person->id);
Yii::$app->user->login($identity);
// to do - update user profile with first and last name
$this->redirect(['meeting/view','id'=>$model->meeting_id]);
} else {
*/
// caution - don't turn off this requirement
// a person could add a celebrity to a meeting by using their email with any meeting code and login with their account
Yii::$app->getSession()->setFlash('warning', Yii::t('frontend','Since you have an account already, please login below.'));
return $this->redirect(['/site/login']);
}
}
创建单一会议受限访问模式后,我将再次对其进行修补。
如果我不知道麦克法林多么狡猾,我可能对此没想过。 ew 另一位女王免于中毒。
也许我在大自然中度过了一个漫长的周末,这也让我与天堂有了更多的联系。
管道中有什么?
我希望您喜欢创建安全的URL邀请人们参加会议的这一集。 我想这对于您自己的服务中的其他场景是高度可重用的。
您可能还可以告诉我,我现在正在享受潜在的会议计划者-还是我工作了太长时间。
最终,构建安全的URL邀请还可以为用户提供公共计划页面。 例如,我可以与朋友共享我的公共会议计划者URL,例如,仅在https://meetingplanner.io/username
安排我的时间。
将来,我什至可以扩展会议计划程序,为专业人士提供订阅功能,以便在其公共会议计划程序页面上进行约会。 但是,我还有其他更令人兴奋的想法。 其他商业导向型公司对此领域颇有兴趣。
乘浪回家
如果您还没有, 请立即与Meeting Planner安排您的第一次会议! 尝试共享会议的快捷方式URL,并将其与我们的社论神汤姆·麦克法兰(Tom McFarlin)保密。
您也可以与我联系@reifman 。 我总是乐于接受新功能的想法和主题建议,以供将来的教程使用。 或尝试我们的服务台并打开错误报告或功能请求通知单。
有关众筹的教程也正在编写中 ,因此请关注我们的WeFunder Meeting Planner页面 。
请阅读“ 用PHP构建您的启动”系列,继续关注所有这些以及以后的教程。
相关链接
翻译自: https://code.tutsplus.com/tutorials/building-your-startup-invite-people-via-url--cms-27163