33 |
34 |
密 码
35 |
登录 {{env('APP_NAME')}} 的密码。
36 |
37 |
38 |
39 |
Email
40 |
安全邮箱!用于激活账号或者重置密码。
41 |
42 |
43 | @endsection
44 |
45 | @section('js')
46 | @if (env('POLR_ACCT_CREATION_RECAPTCHA'))
47 |
48 | @endif
49 | @endsection
50 |
--------------------------------------------------------------------------------
/resources/views/about.blade.php:
--------------------------------------------------------------------------------
1 | @extends('layouts.base')
2 |
3 | @section('css')
4 |
5 |
6 | @endsection
7 |
8 | @section('content')
9 |
10 |
11 |
12 |
13 |
14 | @if ($role == "admin")
15 |
16 | 网站信息
17 | 版本: {{env('POLR_VERSION')}}
18 | 创建时间: {{env('POLR_RELDATE')}}
19 | 安装时间: {{env('APP_NAME')}} on {{env('APP_ADDRESS')}} on {{env('POLR_GENERATED_AT')}}
20 |
21 | @endif
22 |
23 |
{{env('APP_NAME')}} 由 Polr 2 驱动, Polr 2 是一个极简的短地址压缩开源程序。
24 |
更多信息请点击项目主页:Github page 或者: 作者主页 .
25 | Polr 遵循 GNU GPL License 协议。
26 |
27 |
28 |
更多信息
29 |
30 | Copyright (C) 2013-2017 Chaoyi Zha
31 |
32 | This program is free software; you can redistribute it and/or
33 | modify it under the terms of the GNU General Public License
34 | as published by the Free Software Foundation; either version 2
35 | of the License, or (at your option) any later version.
36 |
37 | This program is distributed in the hope that it will be useful,
38 | but WITHOUT ANY WARRANTY; without even the implied warranty of
39 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
40 | GNU General Public License for more details.
41 |
42 | You should have received a copy of the GNU General Public License
43 | along with this program; if not, write to the Free Software
44 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
45 |
46 |
47 | @endsection
48 |
49 | @section('js')
50 |
51 | @endsection
52 |
--------------------------------------------------------------------------------
/app/Http/Middleware/ApiMiddleware.php:
--------------------------------------------------------------------------------
1 | input('key');
14 | $response_type = $request->input('response_type');
15 |
16 | if (!$api_key) {
17 | // no API key provided; check whether anonymous API is enabled
18 |
19 | if (env('SETTING_ANON_API')) {
20 | $username = 'ANONIP-' . $request->ip();
21 | }
22 | else {
23 | throw new ApiException('AUTH_ERROR', '需要验证', 401, $response_type);
24 | }
25 | $user = (object) [
26 | 'username' => $username,
27 | 'anonymous' => true
28 | ];
29 | }
30 | else {
31 | $user = User::where('active', 1)
32 | ->where('api_key', $api_key)
33 | ->where('api_active', 1)
34 | ->first();
35 |
36 | if (!$user) {
37 | throw new ApiException('AUTH_ERROR', '需要验证', 401, $response_type);
38 | }
39 | $username = $user->username;
40 | $user->anonymous = false;
41 | }
42 |
43 | $api_limit_reached = ApiHelper::checkUserApiQuota($username);
44 |
45 | if ($api_limit_reached) {
46 | throw new ApiException('QUOTA_EXCEEDED', 'Quota exceeded.', 429, $response_type);
47 | }
48 | return $user;
49 | }
50 |
51 | /**
52 | * Handle an incoming request.
53 | *
54 | * @param \Illuminate\Http\Request $request
55 | * @param \Closure $next
56 | * @return mixed
57 | */
58 |
59 | public function handle($request, Closure $next) {
60 | $request->user = $this->getApiUserInfo($request);
61 |
62 | return $next($request);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/resources/views/index.blade.php:
--------------------------------------------------------------------------------
1 | @extends('layouts.base')
2 |
3 | @section('css')
4 |
5 | @endsection
6 |
7 | @section('content')
8 |
{{env('APP_NAME')}}
9 |
43 |
44 | Loading Tips...
45 |
46 |
47 | @endsection
48 |
49 | @section('js')
50 |
51 | @endsection
52 |
--------------------------------------------------------------------------------
/app/Http/Controllers/AdminController.php:
--------------------------------------------------------------------------------
1 | isLoggedIn()) {
19 | return redirect(route('login'))->with('error', '请登录进入用户中心。');
20 | }
21 |
22 | $username = session('username');
23 | $role = session('role');
24 |
25 | $user = UserHelper::getUserByUsername($username);
26 |
27 | if (!$user) {
28 | return redirect(route('index'))->with('error', '账号不存在或已禁用。');
29 | }
30 |
31 | return view('admin', [
32 | 'role' => $role,
33 | 'admin_role' => UserHelper::$USER_ROLES['admin'],
34 | 'user_roles' => UserHelper::$USER_ROLES,
35 | 'api_key' => $user->api_key,
36 | 'api_active' => $user->api_active,
37 | 'api_quota' => $user->api_quota,
38 | 'user_id' => $user->id
39 | ]);
40 | }
41 |
42 | public function changePassword(Request $request) {
43 | if (!$this->isLoggedIn()) {
44 | return abort(404);
45 | }
46 |
47 | $username = session('username');
48 | $old_password = $request->input('current_password');
49 | $new_password = $request->input('new_password');
50 |
51 | if (UserHelper::checkCredentials($username, $old_password) == false) {
52 | // Invalid credentials
53 | return redirect('admin')->with('error', '原始密码不正确,请重试。');
54 | }
55 | else {
56 | // Credentials are correct
57 | $user = UserHelper::getUserByUsername($username);
58 | $user->password = Hash::make($new_password);
59 | $user->save();
60 |
61 | $request->session()->flash('success', "密码修改成功!");
62 | return redirect(route('admin'));
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/app/Helpers/StatsHelper.php:
--------------------------------------------------------------------------------
1 | link_id = $link_id;
13 | $this->left_bound_parsed = Carbon::parse($left_bound);
14 | $this->right_bound_parsed = Carbon::parse($right_bound);
15 |
16 | if (!$this->left_bound_parsed->lte($this->right_bound_parsed)) {
17 | // If left bound is not less than or equal to right bound
18 | throw new \Exception('Invalid bounds.');
19 | }
20 |
21 | $days_diff = $this->left_bound_parsed->diffInDays($this->right_bound_parsed);
22 | $max_days_diff = env('_ANALYTICS_MAX_DAYS_DIFF') ?: 365;
23 |
24 | if ($days_diff > $max_days_diff) {
25 | throw new \Exception('Bounds too broad.');
26 | }
27 | }
28 |
29 | public function getBaseRows() {
30 | /**
31 | * Fetches base rows given left date bound, right date bound, and link id
32 | *
33 | * @param integer $link_id
34 | * @param string $left_bound
35 | * @param string $right_bound
36 | *
37 | * @return DB rows
38 | */
39 |
40 | return DB::table('clicks')
41 | ->where('link_id', $this->link_id)
42 | ->where('created_at', '>=', $this->left_bound_parsed)
43 | ->where('created_at', '<=', $this->right_bound_parsed);
44 | }
45 |
46 | public function getDayStats() {
47 | // Return stats by day from the last 30 days
48 | // date => x
49 | // clicks => y
50 | $stats = $this->getBaseRows()
51 | ->select(DB::raw("DATE_FORMAT(created_at, '%Y-%m-%d') AS x, count(*) AS y"))
52 | ->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m-%d')"))
53 | ->orderBy('x', 'asc')
54 | ->get();
55 |
56 | return $stats;
57 | }
58 |
59 | public function getCountryStats() {
60 | $stats = $this->getBaseRows()
61 | ->select(DB::raw("country AS label, count(*) AS clicks"))
62 | ->groupBy('country')
63 | ->orderBy('clicks', 'desc')
64 | ->get();
65 |
66 | return $stats;
67 | }
68 |
69 | public function getRefererStats() {
70 | $stats = $this->getBaseRows()
71 | ->select(DB::raw("COALESCE(referer_host, 'Direct') as label, count(*) as clicks"))
72 | ->groupBy('referer_host')
73 | ->orderBy('clicks', 'desc')
74 | ->get();
75 |
76 | return $stats;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Api/ApiLinkController.php:
--------------------------------------------------------------------------------
1 | input('response_type');
12 | $user = $request->user;
13 |
14 | // Validate parameters
15 | // Encode spaces as %20 to avoid validator conflicts
16 | $validator = \Validator::make(array_merge([
17 | 'url' => str_replace(' ', '%20', $request->input('url'))
18 | ], $request->except('url')), [
19 | 'url' => 'required|url'
20 | ]);
21 |
22 | if ($validator->fails()) {
23 | throw new ApiException('MISSING_PARAMETERS', 'Invalid or missing parameters.', 400, $response_type);
24 | }
25 |
26 | $long_url = $request->input('url'); // * required
27 | $is_secret = ($request->input('is_secret') == 'true' ? true : false);
28 |
29 | $link_ip = $request->ip();
30 | $custom_ending = $request->input('custom_ending');
31 |
32 | try {
33 | $formatted_link = LinkFactory::createLink($long_url, $is_secret, $custom_ending, $link_ip, $user->username, false, true);
34 | }
35 | catch (\Exception $e) {
36 | throw new ApiException('CREATION_ERROR', $e->getMessage(), 400, $response_type);
37 | }
38 |
39 | return self::encodeResponse($formatted_link, 'shorten', $response_type);
40 | }
41 |
42 | public function lookupLink(Request $request) {
43 | $user = $request->user;
44 | $response_type = $request->input('response_type');
45 |
46 | // Validate URL form data
47 | $validator = \Validator::make($request->all(), [
48 | 'url_ending' => 'required|alpha_dash'
49 | ]);
50 |
51 | if ($validator->fails()) {
52 | throw new ApiException('MISSING_PARAMETERS', 'Invalid or missing parameters.', 400, $response_type);
53 | }
54 |
55 | $url_ending = $request->input('url_ending');
56 |
57 | // "secret" key required for lookups on secret URLs
58 | $url_key = $request->input('url_key');
59 |
60 | $link = LinkHelper::linkExists($url_ending);
61 |
62 | if ($link['secret_key']) {
63 | if ($url_key != $link['secret_key']) {
64 | throw new ApiException('ACCESS_DENIED', 'Invalid URL code for secret URL.', 401, $response_type);
65 | }
66 | }
67 |
68 | if ($link) {
69 | return self::encodeResponse([
70 | 'long_url' => $link['long_url'],
71 | 'created_at' => $link['created_at'],
72 | 'clicks' => $link['clicks'],
73 | 'updated_at' => $link['updated_at'],
74 | 'created_at' => $link['created_at']
75 | ], 'lookup', $response_type, $link['long_url']);
76 | }
77 | else {
78 | throw new ApiException('NOT_FOUND', 'Link not found.', 404, $response_type);
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Api/ApiAnalyticsController.php:
--------------------------------------------------------------------------------
1 | user;
13 | $response_type = $request->input('response_type') ?: 'json';
14 |
15 | if ($user->anonymous) {
16 | throw new ApiException('AUTH_ERROR', 'Anonymous access of this API is not permitted.', 401, $response_type);
17 | }
18 |
19 | if ($response_type != 'json') {
20 | throw new ApiException('JSON_ONLY', 'Only JSON-encoded data is available for this endpoint.', 401, $response_type);
21 | }
22 |
23 | $validator = \Validator::make($request->all(), [
24 | 'url_ending' => 'required|alpha_dash',
25 | 'stats_type' => 'alpha_num',
26 | 'left_bound' => 'date',
27 | 'right_bound' => 'date'
28 | ]);
29 |
30 | if ($validator->fails()) {
31 | throw new ApiException('MISSING_PARAMETERS', 'Invalid or missing parameters.', 400, $response_type);
32 | }
33 |
34 | $url_ending = $request->input('url_ending');
35 | $stats_type = $request->input('stats_type');
36 | $left_bound = $request->input('left_bound');
37 | $right_bound = $request->input('right_bound');
38 | $stats_type = $request->input('stats_type');
39 |
40 | // ensure user can only read own analytics or user is admin
41 | $link = LinkHelper::linkExists($url_ending);
42 |
43 | if ($link === false) {
44 | throw new ApiException('NOT_FOUND', 'Link not found.', 404, $response_type);
45 | }
46 |
47 | if (($link->creator != $user->username) &&
48 | !(UserHelper::userIsAdmin($user->username))){
49 | // If user does not own link and is not an admin
50 | throw new ApiException('ACCESS_DENIED', 'Unauthorized.', 401, $response_type);
51 | }
52 |
53 | try {
54 | $stats = new StatsHelper($link->id, $left_bound, $right_bound);
55 | }
56 | catch (\Exception $e) {
57 | throw new ApiException('ANALYTICS_ERROR', $e->getMessage(), 400, $response_type);
58 | }
59 |
60 | if ($stats_type == 'day') {
61 | $fetched_stats = $stats->getDayStats();
62 | }
63 | else if ($stats_type == 'country') {
64 | $fetched_stats = $stats->getCountryStats();
65 | }
66 | else if ($stats_type == 'referer') {
67 | $fetched_stats = $stats->getRefererStats();
68 | }
69 | else {
70 | throw new ApiException('INVALID_ANALYTICS_TYPE', 'Invalid analytics type requested.', 400, $response_type);
71 | }
72 |
73 | return self::encodeResponse([
74 | 'url_ending' => $link->short_url,
75 | 'data' => $fetched_stats,
76 | ], 'data_link_' . $stats_type, $response_type, false);
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/resources/views/layouts/base.blade.php:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
22 |
23 |
@section('title'){{env('APP_NAME')}}@show
24 |
25 |
26 |
27 | {{-- Leave this for stats --}}
28 |
29 | @yield('meta')
30 |
31 | {{-- Load Stylesheets --}}
32 | @if (env('APP_STYLESHEET'))
33 |
34 | @else
35 |
36 | @endif
37 |
38 |
39 |
40 |
41 |
42 |
43 | @yield('css')
44 |
45 |
46 | @include('snippets.navbar')
47 |
48 |
49 | @yield('content')
50 |
51 |
52 |
53 | {{-- Load JavaScript dependencies --}}
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
82 |
83 | @yield('js')
84 |
85 |
86 |
--------------------------------------------------------------------------------
/app/Http/Controllers/StatsController.php:
--------------------------------------------------------------------------------
1 | all(), [
17 | 'left_bound' => 'date',
18 | 'right_bound' => 'date'
19 | ]);
20 |
21 | if ($validator->fails() && !session('error')) {
22 | // Do not flash error if there is already an error flashed
23 | return redirect()->back()->with('error', 'Invalid date bounds.');
24 | }
25 |
26 | $user_left_bound = $request->input('left_bound');
27 | $user_right_bound = $request->input('right_bound');
28 |
29 | // Carbon bounds for StatHelper
30 | $left_bound = $user_left_bound ?: Carbon::now()->subDays(self::DAYS_TO_FETCH);
31 | $right_bound = $user_right_bound ?: Carbon::now();
32 |
33 | if (Carbon::parse($right_bound)->gt(Carbon::now()) && !session('error')) {
34 | // Right bound must not be greater than current time
35 | // i.e cannot be in the future
36 | return redirect()->back()->with('error', 'Right date bound cannot be in the future.');
37 | }
38 |
39 | if (!$this->isLoggedIn()) {
40 | return redirect(route('login'))->with('error', '请登录查看统计信息。');
41 | }
42 |
43 | $link = Link::where('short_url', $short_url)
44 | ->first();
45 |
46 | // Return 404 if link not found
47 | if ($link == null) {
48 | return redirect(route('admin'))->with('error', '链接不存在。');
49 | }
50 | if (!env('SETTING_ADV_ANALYTICS')) {
51 | return redirect(route('login'))->with('error', '功能未开启。');
52 | }
53 |
54 | $link_id = $link->id;
55 |
56 | if ( (session('username') != $link->creator) && !self::currIsAdmin() ) {
57 | return redirect(route('admin'))->with('error', '无权查看。');
58 | }
59 |
60 | try {
61 | // Initialize StatHelper
62 | $stats = new StatsHelper($link_id, $left_bound, $right_bound);
63 | }
64 | catch (\Exception $e) {
65 | if (!session('error')) {
66 | // Do not flash error if there is already an error flashed
67 | return redirect()->back()->with('error', 'Invalid date bounds.
68 | The right date bound must be more recent than the left bound.');
69 | }
70 | }
71 |
72 | $day_stats = $stats->getDayStats();
73 | $country_stats = $stats->getCountryStats();
74 | $referer_stats = $stats->getRefererStats();
75 |
76 | return view('link_stats', [
77 | 'link' => $link,
78 | 'day_stats' => $day_stats,
79 | 'country_stats' => $country_stats,
80 | 'referer_stats' => $referer_stats,
81 |
82 | 'left_bound' => ($user_left_bound ?: $left_bound->toDateTimeString()),
83 | 'right_bound' => ($user_right_bound ?: $right_bound->toDateTimeString()),
84 |
85 | 'no_div_padding' => true
86 | ]);
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/app/Helpers/UserHelper.php:
--------------------------------------------------------------------------------
1 | 'admin',
11 | 'default' => '',
12 | ];
13 |
14 | public static function userExists($username) {
15 | /* XXX: used primarily with test cases */
16 |
17 | $user = self::getUserByUsername($username, $inactive=true);
18 |
19 | return ($user ? true : false);
20 | }
21 |
22 | public static function emailExists($email) {
23 | /* XXX: used primarily with test cases */
24 |
25 | $user = self::getUserByEmail($email, $inactive=true);
26 |
27 | return ($user ? true : false);
28 | }
29 |
30 | public static function validateUsername($username) {
31 | return ctype_alnum($username);
32 | }
33 |
34 | public static function userIsAdmin($username) {
35 | return (self::getUserByUsername($username)->role == self::$USER_ROLES['admin']);
36 | }
37 |
38 | public static function checkCredentials($username, $password) {
39 | $user = User::where('active', 1)
40 | ->where('username', $username)
41 | ->first();
42 |
43 | if ($user == null) {
44 | return false;
45 | }
46 |
47 | $correct_password = Hash::check($password, $user->password);
48 |
49 | if (!$correct_password) {
50 | return false;
51 | }
52 | else {
53 | return ['username' => $username, 'role' => $user->role];
54 | }
55 | }
56 |
57 | public static function resetRecoveryKey($username) {
58 | $recovery_key = CryptoHelper::generateRandomHex(50);
59 | $user = self::getUserByUsername($username);
60 |
61 | if (!$user) {
62 | return false;
63 | }
64 |
65 | $user->recovery_key = $recovery_key;
66 | $user->save();
67 |
68 | return $recovery_key;
69 | }
70 |
71 | public static function userResetKeyCorrect($username, $recovery_key, $inactive=false) {
72 | // Given a username and a recovery key, return true if they match.
73 | $user = self::getUserByUsername($username, $inactive);
74 |
75 | if ($user) {
76 | if ($recovery_key != $user->recovery_key) {
77 | return false;
78 | }
79 | }
80 | else {
81 | return false;
82 | }
83 | return true;
84 | }
85 |
86 | public static function getUserBy($attr, $value, $inactive=false) {
87 | $user = User::where($attr, $value);
88 |
89 | if (!$inactive) {
90 | // if user must be active
91 | $user = $user
92 | ->where('active', 1);
93 | }
94 |
95 | return $user->first();
96 | }
97 |
98 | public static function getUserById($user_id, $inactive=false) {
99 | return self::getUserBy('id', $user_id, $inactive);
100 | }
101 |
102 | public static function getUserByUsername($username, $inactive=false) {
103 | return self::getUserBy('username', $username, $inactive);
104 | }
105 |
106 | public static function getUserByEmail($email, $inactive=false) {
107 | return self::getUserBy('email', $email, $inactive);
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/app/Exceptions/Handler.php:
--------------------------------------------------------------------------------
1 | to(env('SETTING_INDEX_REDIRECT'));
52 | }
53 | // Otherwise, show a nice error page
54 | return response(view('errors.404'), 404);
55 | }
56 | if ($e instanceof HttpException) {
57 | // Handle HTTP exceptions thrown by public-facing controllers
58 | $status_code = $e->getStatusCode();
59 | $status_message = $e->getMessage();
60 |
61 | if ($status_code == 500) {
62 | // Render a nice error page for 500s
63 | return response(view('errors.500'), 500);
64 | }
65 | else {
66 | // If not 500, render generic page
67 | return response(
68 | view('errors.generic', [
69 | 'status_code' => $status_code,
70 | 'status_message' => $status_message
71 | ]), $status_code);
72 | }
73 | }
74 | if ($e instanceof ApiException) {
75 | // Handle HTTP exceptions thrown by API controllers
76 | $status_code = $e->getCode();
77 | $encoded_status_message = $e->getEncodedErrorMessage();
78 | if ($e->response_type == 'json') {
79 | return response($encoded_status_message, $status_code)
80 | ->header('Content-Type', 'application/json')
81 | ->header('Access-Control-Allow-Origin', '*');
82 | }
83 |
84 | return response($encoded_status_message, $status_code)
85 | ->header('Content-Type', 'text/plain')
86 | ->header('Access-Control-Allow-Origin', '*');
87 | }
88 | }
89 |
90 | return parent::render($request, $e);
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/public/js/index.js:
--------------------------------------------------------------------------------
1 | $(function() {
2 | var optionsButton = $('#show-link-options');
3 | $('#options').hide();
4 | var slide = 0;
5 | optionsButton.click(function() {
6 | if (slide === 0) {
7 | $("#options").slideDown();
8 | slide = 1;
9 | } else {
10 | $("#options").slideUp();
11 | slide = 0;
12 | }
13 | });
14 | $('#check-link-availability').click(function() {
15 | var custom_link = $('.custom-url-field').val();
16 | var request = $.ajax({
17 | url: "/api/v2/link_avail_check",
18 | type: "POST",
19 | data: {
20 | link_ending: custom_link
21 | },
22 | dataType: "html"
23 | });
24 | $('#link-availability-status').html('
Loading');
25 | request.done(function(msg) {
26 | if (msg == 'unavailable') {
27 | $('#link-availability-status').html('
已存在');
28 | } else if (msg == 'available') {
29 | $('#link-availability-status').html('
可用');
30 | } else if (msg == 'invalid') {
31 | $('#link-availability-status').html('
请输入英文和大小写字母');
32 | } else {
33 | $('#link-availability-status').html('
请重试:' + msg);
34 | }
35 | });
36 |
37 | request.fail(function(jqXHR, textStatus) {
38 | $('#link-availability-status').html('
请重试:' + textstatus);
39 | });
40 | });
41 | min = 1;
42 | max = 2;
43 | var i = Math.floor(Math.random() * (max - min + 1)) + min;
44 | changeTips(i);
45 | var tipstimer = setInterval(function() {
46 | changeTips(i);
47 | i++;
48 | }, 8000);
49 |
50 | function setTip(tip) {
51 | $("#tips").html(tip);
52 | }
53 |
54 | function changeTips(tcase) {
55 | switch (tcase) {
56 | case 1:
57 | setTip('Create an account to keep track of your links');
58 | break;
59 | case 2:
60 | setTip('Did you know you can change the URL ending by clicking on "Link Options"?');
61 | i = 1;
62 | break;
63 | }
64 | }
65 | });
66 |
67 | $(function() {
68 | // Setup drop down menu
69 | $('.dropdown-toggle').dropdown();
70 |
71 | // Fix input element click problem
72 | $('.dropdown input, .dropdown label').click(function(e) {
73 | e.stopPropagation();
74 | });
75 | $('.btn-toggle').click(function() {
76 | $(this).find('.btn').toggleClass('active');
77 |
78 | if ($(this).find('.btn-primary').size() > 0) {
79 | $(this).find('.btn').toggleClass('btn-primary');
80 | }
81 | if ($(this).find('.btn-danger').size() > 0) {
82 | $(this).find('.btn').toggleClass('btn-danger');
83 | }
84 | if ($(this).find('.btn-success').size() > 0) {
85 | $(this).find('.btn').toggleClass('btn-success');
86 | }
87 | if ($(this).find('.btn-info').size() > 0) {
88 | $(this).find('.btn').toggleClass('btn-info');
89 | }
90 |
91 | $(this).find('.btn').toggleClass('btn-default');
92 |
93 | });
94 | });
95 |
--------------------------------------------------------------------------------
/app/Http/Controllers/LinkController.php:
--------------------------------------------------------------------------------
1 | with('error', $message);
21 | }
22 |
23 | public function performShorten(Request $request) {
24 | if (env('SETTING_SHORTEN_PERMISSION') && !self::isLoggedIn()) {
25 | return redirect(route('index'))->with('error', 'You must be logged in to shorten links.');
26 | }
27 |
28 | // Validate URL form data
29 | $this->validate($request, [
30 | 'link-url' => 'required|url',
31 | 'custom-ending' => 'alpha_dash'
32 | ]);
33 |
34 | $long_url = $request->input('link-url');
35 | $custom_ending = $request->input('custom-ending');
36 | $is_secret = ($request->input('options') == "s" ? true : false);
37 | $creator = session('username');
38 | $link_ip = $request->ip();
39 |
40 | try {
41 | $short_url = LinkFactory::createLink($long_url, $is_secret, $custom_ending, $link_ip, $creator);
42 | }
43 | catch (\Exception $e) {
44 | return self::renderError($e->getMessage());
45 | }
46 |
47 | return view('shorten_result', ['short_url' => $short_url]);
48 | }
49 |
50 | public function performRedirect(Request $request, $short_url, $secret_key=false) {
51 | $link = Link::where('short_url', $short_url)
52 | ->first();
53 |
54 | // Return 404 if link not found
55 | if ($link == null) {
56 | return abort(404);
57 | }
58 |
59 | // Return an error if the link has been disabled
60 | // or return a 404 if SETTING_REDIRECT_404 is set to true
61 | if ($link->is_disabled == 1) {
62 | if (env('SETTING_REDIRECT_404')) {
63 | return abort(404);
64 | }
65 |
66 | return view('error', [
67 | 'message' => '该链接已被管理员禁用。'
68 | ]);
69 | }
70 |
71 | // Return a 403 if the secret key is incorrect
72 | $link_secret_key = $link->secret_key;
73 | if ($link_secret_key) {
74 | if (!$secret_key) {
75 | // if we do not receieve a secret key
76 | // when we are expecting one, return a 403
77 | return abort(403);
78 | }
79 | else {
80 | if ($link_secret_key != $secret_key) {
81 | // a secret key is provided, but it is incorrect
82 | return abort(403);
83 | }
84 | }
85 | }
86 |
87 | // Increment click count
88 | $long_url = $link->long_url;
89 | $clicks = intval($link->clicks);
90 |
91 | if (is_int($clicks)) {
92 | $clicks += 1;
93 | }
94 | $link->clicks = $clicks;
95 | $link->save();
96 |
97 | if (env('SETTING_ADV_ANALYTICS')) {
98 | // Record advanced analytics if option is enabled
99 | ClickHelper::recordClick($link, $request);
100 | }
101 | // Redirect to final destination
102 | return redirect()->to($long_url, 301);
103 | }
104 |
105 | }
106 |
--------------------------------------------------------------------------------
/resources/views/snippets/navbar.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 切换导航
5 |
6 |
7 |
8 |
9 |
10 |
11 |
14 |
15 |
16 | 关 于
17 |
18 | @if (empty(session('username')))
19 | 登 录
20 | @if (env('POLR_ALLOW_ACCT_CREATION'))
21 | 注 册
22 | @endif
23 | @else
24 | 仪表盘
25 | 设 置
26 | 退 出
27 | @endif
28 |
29 |
30 |
31 |
32 |
33 | @if (empty(session('username')))
34 | @if (env('POLR_ALLOW_ACCT_CREATION'))
35 | 注 册
36 | @endif
37 |
38 |
39 | 登 录
40 |
49 |
50 | @else
51 |
61 | @endif
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/bootstrap/app.php:
--------------------------------------------------------------------------------
1 | withFacades();
23 | $app->withEloquent();
24 |
25 | $app->configure('geoip');
26 |
27 | /*
28 | |--------------------------------------------------------------------------
29 | | Register Container Bindings
30 | |--------------------------------------------------------------------------
31 | |
32 | | Now we will register a few bindings in the service container. We will
33 | | register the exception handler and the console kernel. You may add
34 | | your own bindings here if you like or you can make another file.
35 | |
36 | */
37 |
38 | $app->singleton(
39 | Illuminate\Contracts\Debug\ExceptionHandler::class,
40 | App\Exceptions\Handler::class
41 | );
42 |
43 | $app->singleton(
44 | Illuminate\Contracts\Console\Kernel::class,
45 | App\Console\Kernel::class
46 | );
47 |
48 | /*
49 | |--------------------------------------------------------------------------
50 | | Register Middleware
51 | |--------------------------------------------------------------------------
52 | |
53 | | Next, we will register the middleware with the application. These can
54 | | be global middleware that run before and after each request into a
55 | | route or middleware that'll be assigned to some specific routes.
56 | |
57 | */
58 |
59 | $app->middleware([
60 | Illuminate\Cookie\Middleware\EncryptCookies::class,
61 | // Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
62 | Illuminate\Session\Middleware\StartSession::class,
63 | Illuminate\View\Middleware\ShareErrorsFromSession::class,
64 | App\Http\Middleware\VerifyCsrfToken::class,
65 | ]);
66 |
67 | $app->routeMiddleware([
68 | 'api' => App\Http\Middleware\ApiMiddleware::class,
69 | ]);
70 |
71 | /*
72 | |--------------------------------------------------------------------------
73 | | Register Service Providers
74 | |--------------------------------------------------------------------------
75 | |
76 | | Here we will register all of the application's service providers which
77 | | are used to bind services into the container. Service providers are
78 | | totally optional, so you are not required to uncomment this line.
79 | |
80 | */
81 |
82 | $app->register(App\Providers\AppServiceProvider::class);
83 | $app->register(\Yajra\Datatables\DatatablesServiceProvider::class);
84 | $app->register(\Torann\GeoIP\GeoIPServiceProvider::class);
85 | // $app->register(App\Providers\EventServiceProvider::class);
86 |
87 | /*
88 | |--------------------------------------------------------------------------
89 | | Load The Application Routes
90 | |--------------------------------------------------------------------------
91 | |
92 | | Next we will include the routes file so that they can all be added to
93 | | the application. This will provide all of the URLs the application
94 | | can respond to, as well as the controllers that may handle them.
95 | |
96 | */
97 |
98 | $app->group(['namespace' => 'App\Http\Controllers'], function ($app) {
99 | require __DIR__.'/../app/Http/routes.php';
100 | });
101 |
102 |
103 | return $app;
104 |
--------------------------------------------------------------------------------
/app/Factories/LinkFactory.php:
--------------------------------------------------------------------------------
1 | self::MAXIMUM_LINK_LENGTH) {
45 | // If $long_url is longer than the maximum length, then
46 | // throw an Exception
47 | throw new \Exception('长度超出限制。');
48 | }
49 |
50 | $is_already_short = LinkHelper::checkIfAlreadyShortened($long_url);
51 |
52 | if ($is_already_short) {
53 | throw new \Exception('这个链接已经是短链接了。');
54 | }
55 |
56 | if (!$is_secret && (!isset($custom_ending) || $custom_ending === '') && (LinkHelper::longLinkExists($long_url, $creator) !== false)) {
57 | // if link is not specified as secret, is non-custom, and
58 | // already exists in Polr, lookup the value and return
59 | $existing_link = LinkHelper::longLinkExists($long_url, $creator);
60 | return self::formatLink($existing_link);
61 | }
62 |
63 | if (isset($custom_ending) && $custom_ending !== '') {
64 | // has custom ending
65 | $ending_conforms = LinkHelper::validateEnding($custom_ending);
66 | if (!$ending_conforms) {
67 | throw new \Exception('只能使用数字和大小写字母。');
68 | }
69 |
70 | $ending_in_use = LinkHelper::linkExists($custom_ending);
71 | if ($ending_in_use) {
72 | throw new \Exception('这个短链接已使用。');
73 | }
74 |
75 | $link_ending = $custom_ending;
76 | }
77 | else {
78 | if (env('SETTING_PSEUDORANDOM_ENDING')) {
79 | // generate a pseudorandom ending
80 | $link_ending = LinkHelper::findPseudoRandomEnding();
81 | }
82 | else {
83 | // generate a counter-based ending or use existing ending if possible
84 | $link_ending = LinkHelper::findSuitableEnding();
85 | }
86 | }
87 |
88 | $link = new Link;
89 | $link->short_url = $link_ending;
90 | $link->long_url = $long_url;
91 | $link->ip = $link_ip;
92 | $link->is_custom = $custom_ending != null;
93 |
94 | $link->is_api = $is_api;
95 |
96 | if ($creator) {
97 | // if user is logged in, save user as creator
98 | $link->creator = $creator;
99 | }
100 |
101 | if ($is_secret) {
102 | $rand_bytes_num = intval(env('POLR_SECRET_BYTES'));
103 | $secret_key = CryptoHelper::generateRandomHex($rand_bytes_num);
104 | $link->secret_key = $secret_key;
105 | }
106 | else {
107 | $secret_key = false;
108 | }
109 |
110 | $link->save();
111 |
112 | $formatted_link = self::formatLink($link_ending, $secret_key);
113 |
114 | if ($return_object) {
115 | return $link;
116 | }
117 |
118 | return $formatted_link;
119 | }
120 |
121 | }
122 |
--------------------------------------------------------------------------------
/app/Helpers/LinkHelper.php:
--------------------------------------------------------------------------------
1 | first();
47 |
48 | if ($link != null) {
49 | return $link;
50 | }
51 | else {
52 | return false;
53 | }
54 | }
55 |
56 | static public function longLinkExists($long_url, $username=false) {
57 | /**
58 | * Provided a long link (string),
59 | * check whether the link is in the DB.
60 | * If a username is provided, only search for links created by the
61 | * user.
62 | * @return boolean
63 | */
64 | $link_base = Link::longUrl($long_url)
65 | ->where('is_custom', 0)
66 | ->where('secret_key', '');
67 |
68 | if (is_null($username)) {
69 | // Search for links without a creator only
70 | $link = $link_base->where('creator', '')->first();
71 | }
72 | else if (($username !== false)) {
73 | // Search for links created by $username only
74 | $link = $link_base->where('creator', $username)->first();
75 | }
76 | else {
77 | // Search for links created by any user
78 | $link = $link_base->first();
79 | }
80 |
81 | if ($link == null) {
82 | return false;
83 | }
84 | else {
85 | return $link->short_url;
86 | }
87 | }
88 |
89 | static public function validateEnding($link_ending) {
90 | $is_valid_ending = preg_match('/^[a-zA-Z0-9-_]+$/', $link_ending);
91 | return $is_valid_ending;
92 | }
93 |
94 | static public function findPseudoRandomEnding() {
95 | /**
96 | * Return an available pseudorandom string of length _PSEUDO_RANDOM_KEY_LENGTH,
97 | * as defined in .env
98 | * Edit _PSEUDO_RANDOM_KEY_LENGTH in .env if you wish to increase the length
99 | * of the pseudorandom string generated.
100 | * @return string
101 | */
102 |
103 | $pr_str = '';
104 | $in_use = true;
105 |
106 | while ($in_use) {
107 | // Generate a new string until the ending is not in use
108 | $pr_str = str_random(env('_PSEUDO_RANDOM_KEY_LENGTH'));
109 | $in_use = LinkHelper::linkExists($pr_str);
110 | }
111 |
112 | return $pr_str;
113 | }
114 |
115 | static public function findSuitableEnding() {
116 | /**
117 | * Provided an in-use link ending (string),
118 | * find the next available base-32/62 ending.
119 | * @return string
120 | */
121 | $base = env('POLR_BASE');
122 |
123 | $link = Link::where('is_custom', 0)
124 | ->orderBy('created_at', 'desc')
125 | ->first();
126 |
127 | if ($link == null) {
128 | $base10_val = 0;
129 | $base_x_val = 0;
130 | }
131 | else {
132 | $latest_link_ending = $link->short_url;
133 | $base10_val = BaseHelper::toBase10($latest_link_ending, $base);
134 | $base10_val++;
135 | }
136 |
137 |
138 | $base_x_val = null;
139 |
140 | while (LinkHelper::linkExists($base_x_val) || $base_x_val == null) {
141 | $base_x_val = BaseHelper::toBase($base10_val, $base);
142 | $base10_val++;
143 | }
144 |
145 | return $base_x_val;
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/public/js/toastr.min.js:
--------------------------------------------------------------------------------
1 | !function(e){e(["jquery"],function(e){return function(){function t(e,t,n){return g({type:O.error,iconClass:m().iconClasses.error,message:e,optionsOverride:n,title:t})}function n(t,n){return t||(t=m()),v=e("#"+t.containerId),v.length?v:(n&&(v=u(t)),v)}function i(e,t,n){return g({type:O.info,iconClass:m().iconClasses.info,message:e,optionsOverride:n,title:t})}function o(e){w=e}function s(e,t,n){return g({type:O.success,iconClass:m().iconClasses.success,message:e,optionsOverride:n,title:t})}function a(e,t,n){return g({type:O.warning,iconClass:m().iconClasses.warning,message:e,optionsOverride:n,title:t})}function r(e,t){var i=m();v||n(i),l(e,i,t)||d(i)}function c(t){var i=m();return v||n(i),t&&0===e(":focus",t).length?void h(t):void(v.children().length&&v.remove())}function d(t){for(var n=v.children(),i=n.length-1;i>=0;i--)l(e(n[i]),t)}function l(t,n,i){var o=i&&i.force?i.force:!1;return t&&(o||0===e(":focus",t).length)?(t[n.hideMethod]({duration:n.hideDuration,easing:n.hideEasing,complete:function(){h(t)}}),!0):!1}function u(t){return v=e("
").attr("id",t.containerId).addClass(t.positionClass).attr("aria-live","polite").attr("role","alert"),v.appendTo(e(t.target)),v}function p(){return{tapToDismiss:!0,toastClass:"toast",containerId:"toast-container",debug:!1,showMethod:"fadeIn",showDuration:300,showEasing:"swing",onShown:void 0,hideMethod:"fadeOut",hideDuration:1e3,hideEasing:"swing",onHidden:void 0,closeMethod:!1,closeDuration:!1,closeEasing:!1,extendedTimeOut:1e3,iconClasses:{error:"toast-error",info:"toast-info",success:"toast-success",warning:"toast-warning"},iconClass:"toast-info",positionClass:"toast-top-right",timeOut:5e3,titleClass:"toast-title",messageClass:"toast-message",escapeHtml:!1,target:"body",closeHtml:'
× ',newestOnTop:!0,preventDuplicates:!1,progressBar:!1}}function f(e){w&&w(e)}function g(t){function i(e){return null==e&&(e=""),new String(e).replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">")}function o(){r(),d(),l(),u(),p(),c()}function s(){y.hover(b,O),!x.onclick&&x.tapToDismiss&&y.click(w),x.closeButton&&k&&k.click(function(e){e.stopPropagation?e.stopPropagation():void 0!==e.cancelBubble&&e.cancelBubble!==!0&&(e.cancelBubble=!0),w(!0)}),x.onclick&&y.click(function(e){x.onclick(e),w()})}function a(){y.hide(),y[x.showMethod]({duration:x.showDuration,easing:x.showEasing,complete:x.onShown}),x.timeOut>0&&(H=setTimeout(w,x.timeOut),q.maxHideTime=parseFloat(x.timeOut),q.hideEta=(new Date).getTime()+q.maxHideTime,x.progressBar&&(q.intervalId=setInterval(D,10)))}function r(){t.iconClass&&y.addClass(x.toastClass).addClass(E)}function c(){x.newestOnTop?v.prepend(y):v.append(y)}function d(){t.title&&(I.append(x.escapeHtml?i(t.title):t.title).addClass(x.titleClass),y.append(I))}function l(){t.message&&(M.append(x.escapeHtml?i(t.message):t.message).addClass(x.messageClass),y.append(M))}function u(){x.closeButton&&(k.addClass("toast-close-button").attr("role","button"),y.prepend(k))}function p(){x.progressBar&&(B.addClass("toast-progress"),y.prepend(B))}function g(e,t){if(e.preventDuplicates){if(t.message===C)return!0;C=t.message}return!1}function w(t){var n=t&&x.closeMethod!==!1?x.closeMethod:x.hideMethod,i=t&&x.closeDuration!==!1?x.closeDuration:x.hideDuration,o=t&&x.closeEasing!==!1?x.closeEasing:x.hideEasing;return!e(":focus",y).length||t?(clearTimeout(q.intervalId),y[n]({duration:i,easing:o,complete:function(){h(y),x.onHidden&&"hidden"!==j.state&&x.onHidden(),j.state="hidden",j.endTime=new Date,f(j)}})):void 0}function O(){(x.timeOut>0||x.extendedTimeOut>0)&&(H=setTimeout(w,x.extendedTimeOut),q.maxHideTime=parseFloat(x.extendedTimeOut),q.hideEta=(new Date).getTime()+q.maxHideTime)}function b(){clearTimeout(H),q.hideEta=0,y.stop(!0,!0)[x.showMethod]({duration:x.showDuration,easing:x.showEasing})}function D(){var e=(q.hideEta-(new Date).getTime())/q.maxHideTime*100;B.width(e+"%")}var x=m(),E=t.iconClass||x.iconClass;if("undefined"!=typeof t.optionsOverride&&(x=e.extend(x,t.optionsOverride),E=t.optionsOverride.iconClass||E),!g(x,t)){T++,v=n(x,!0);var H=null,y=e("
"),I=e("
"),M=e("
"),B=e("
"),k=e(x.closeHtml),q={intervalId:null,hideEta:null,maxHideTime:null},j={toastId:T,state:"visible",startTime:new Date,options:x,map:t};return o(),a(),s(),f(j),x.debug&&console&&console.log(j),y}}function m(){return e.extend({},p(),b.options)}function h(e){v||(v=n()),e.is(":visible")||(e.remove(),e=null,0===v.children().length&&(v.remove(),C=void 0))}var v,w,C,T=0,O={error:"error",info:"info",success:"success",warning:"warning"},b={clear:r,remove:c,error:t,getContainer:n,info:i,options:{},subscribe:o,success:s,version:"2.1.2",warning:a};return b}()})}("function"==typeof define&&define.amd?define:function(e,t){"undefined"!=typeof module&&module.exports?module.exports=t(require("jquery")):window.toastr=t(window.jQuery)});
2 | //# sourceMappingURL=toastr.js.map
--------------------------------------------------------------------------------
/resources/views/link_stats.blade.php:
--------------------------------------------------------------------------------
1 | @extends('layouts.base')
2 |
3 | @section('css')
4 |
5 |
6 |
7 |
8 | @endsection
9 |
10 | @section('content')
11 |
12 |
54 |
55 |
56 |
57 |
Traffic over Time (total: {{ $link->clicks }})
58 |
59 |
60 |
61 |
Traffic sources
62 |
63 |
64 |
65 |
66 |
67 |
71 |
72 |
Referers
73 |
74 |
75 |
76 | Host
77 | Clicks
78 |
79 |
80 |
81 | @foreach ($referer_stats as $referer)
82 |
83 | {{ $referer->label }}
84 | {{ $referer->clicks }}
85 |
86 | @endforeach
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | @endsection
95 |
96 | @section('js')
97 | {{-- Load data --}}
98 |
108 |
109 | {{-- Include extra JS --}}
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 | @endsection
119 |
--------------------------------------------------------------------------------
/config/geoip.php:
--------------------------------------------------------------------------------
1 | true,
16 |
17 | /*
18 | |--------------------------------------------------------------------------
19 | | Include Currency in Results
20 | |--------------------------------------------------------------------------
21 | |
22 | | When enabled the system will do it's best in deciding the user's currency
23 | | by matching their ISO code to a preset list of currencies.
24 | |
25 | */
26 |
27 | 'include_currency' => true,
28 |
29 | /*
30 | |--------------------------------------------------------------------------
31 | | Default Service
32 | |--------------------------------------------------------------------------
33 | |
34 | | Here you may specify the default storage driver that should be used
35 | | by the framework.
36 | |
37 | | Supported: "maxmind_database", "maxmind_api", "ipapi"
38 | |
39 | */
40 |
41 | 'service' => 'maxmind_database',
42 |
43 | /*
44 | |--------------------------------------------------------------------------
45 | | Storage Specific Configuration
46 | |--------------------------------------------------------------------------
47 | |
48 | | Here you may configure as many storage drivers as you wish.
49 | |
50 | */
51 |
52 | 'services' => [
53 |
54 | 'maxmind_database' => [
55 | 'class' => \Torann\GeoIP\Services\MaxMindDatabase::class,
56 | 'database_path' => storage_path('app/geoip.mmdb'),
57 | 'update_url' => 'https://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz',
58 | 'locales' => ['en'],
59 | ],
60 |
61 | 'maxmind_api' => [
62 | 'class' => \Torann\GeoIP\Services\MaxMindWebService::class,
63 | 'user_id' => env('MAXMIND_USER_ID'),
64 | 'license_key' => env('MAXMIND_LICENSE_KEY'),
65 | 'locales' => ['en'],
66 | ],
67 |
68 | 'ipapi' => [
69 | 'class' => \Torann\GeoIP\Services\IPApi::class,
70 | 'secure' => true,
71 | 'key' => env('IPAPI_KEY'),
72 | 'continent_path' => storage_path('app/continents.json'),
73 | ],
74 |
75 | ],
76 |
77 | /*
78 | |--------------------------------------------------------------------------
79 | | Default Cache Driver
80 | |--------------------------------------------------------------------------
81 | |
82 | | Here you may specify the type of caching that should be used
83 | | by the package.
84 | |
85 | | Options:
86 | |
87 | | all - All location are cached
88 | | some - Cache only the requesting user
89 | | none - Disable cached
90 | |
91 | */
92 |
93 | 'cache' => 'none',
94 |
95 | /*
96 | |--------------------------------------------------------------------------
97 | | Cache Tags
98 | |--------------------------------------------------------------------------
99 | |
100 | | Cache tags are not supported when using the file or database cache
101 | | drivers in Laravel. This is done so that only locations can be cleared.
102 | |
103 | */
104 |
105 | // 'cache_tags' => ['torann-geoip-location'],
106 |
107 | /*
108 | |--------------------------------------------------------------------------
109 | | Cache Expiration
110 | |--------------------------------------------------------------------------
111 | |
112 | | Define how long cached location are valid.
113 | |
114 | */
115 |
116 | 'cache_expires' => 30,
117 |
118 | /*
119 | |--------------------------------------------------------------------------
120 | | Default Location
121 | |--------------------------------------------------------------------------
122 | |
123 | | Return when a location is not found.
124 | |
125 | */
126 |
127 | 'default_location' => [
128 | 'ip' => '127.0.0.0',
129 | 'iso_code' => 'US',
130 | 'country' => 'United States',
131 | 'city' => 'New Haven',
132 | 'state' => 'CT',
133 | 'state_name' => 'Connecticut',
134 | 'postal_code' => '06510',
135 | 'lat' => 41.31,
136 | 'lon' => -72.92,
137 | 'timezone' => 'America/New_York',
138 | 'continent' => 'NA',
139 | 'default' => true,
140 | 'currency' => 'USD',
141 | ],
142 |
143 | ];
144 |
--------------------------------------------------------------------------------
/resources/views/env.blade.php:
--------------------------------------------------------------------------------
1 | APP_ENV=production
2 |
3 | # Set to true if debugging
4 | APP_DEBUG=false
5 |
6 | # 32-character key (e.g 3EWBLwxTfh%*f&xRBqdGEIUVvn4%$Hfi)
7 | APP_KEY="{{{$APP_KEY}}}"
8 |
9 | # Your app's name (shown on interface)
10 | APP_NAME="{{$APP_NAME}}"
11 |
12 | # Protocol to access your app. e.g https://
13 | APP_PROTOCOL="{{$APP_PROTOCOL}}"
14 |
15 | # Your app's external address (e.g example.com)
16 | APP_ADDRESS="{{$APP_ADDRESS}}"
17 |
18 | # Your app's bootstrap stylesheet
19 | # e.g https://maxcdn.bootstrapcdn.com/bootswatch/3.3.5/flatly/bootstrap.min.css
20 | APP_STYLESHEET="{{$APP_STYLESHEET}}"
21 |
22 | # Set to today's date (e.g November 3, 2015)
23 | POLR_GENERATED_AT="{{$POLR_GENERATED_AT}}"
24 |
25 | # Set to true after running setup script
26 | # e.g true
27 | POLR_SETUP_RAN={{$POLR_SETUP_RAN}}
28 |
29 | DB_CONNECTION=mysql
30 | # Set to your DB host (e.g localhost)
31 | DB_HOST="{{{$DB_HOST}}}"
32 | # DB port (e.g 3306)
33 | DB_PORT={{$DB_PORT}}
34 | # Set to your DB name (e.g polr)
35 | DB_DATABASE="{{{$DB_DATABASE}}}"
36 | # DB credentials
37 | # e.g root
38 | DB_USERNAME="{{{$DB_USERNAME}}}"
39 | DB_PASSWORD="{{{$DB_PASSWORD}}}"
40 |
41 | # Polr Settings
42 |
43 | # Set to true to show an interface to logged off users
44 | # If false, set the SETTING_INDEX_REDIRECT
45 | # You may login by heading to /login if the public interface is off
46 | SETTING_PUBLIC_INTERFACE={{$ST_PUBLIC_INTERFACE}}
47 |
48 | # Set to true to allow signups, false to disable (e.g true/false)
49 | POLR_ALLOW_ACCT_CREATION={{$POLR_ALLOW_ACCT_CREATION}}
50 |
51 | # Set to true to require activation by email (e.g true/false)
52 | POLR_ACCT_ACTIVATION={{$POLR_ACCT_ACTIVATION}}
53 |
54 | # Set to true to require reCAPTCHAs on sign up pages
55 | # If this setting is enabled, you must also provide your reCAPTCHA keys
56 | # in POLR_RECAPTCHA_SITE_KEY and POLR_RECAPTCHA_SECRET_KEY
57 | POLR_ACCT_CREATION_RECAPTCHA={{$POLR_ACCT_CREATION_RECAPTCHA}}
58 |
59 | # Set to true to require users to be logged in before shortening URLs
60 | SETTING_SHORTEN_PERMISSION={{$ST_SHORTEN_PERMISSION}}
61 |
62 | # You must set SETTING_INDEX_REDIRECT if SETTING_PUBLIC_INTERFACE is false
63 | # Polr will redirect logged off users to this URL
64 | SETTING_INDEX_REDIRECT={{$ST_INDEX_REDIRECT}}
65 |
66 | # Set to true if you wish to redirect 404s to SETTING_INDEX_REDIRECT
67 | # Otherwise, an error message will be shown
68 | SETTING_REDIRECT_404={{$ST_REDIRECT_404}}
69 |
70 | # Set to true to enable password recovery
71 | SETTING_PASSWORD_RECOV={{$ST_PASSWORD_RECOV}}
72 |
73 | # Set to true to generate API keys for each user on registration
74 | SETTING_AUTO_API={{$ST_AUTO_API}}
75 |
76 | # Set to true to allow anonymous API access
77 | SETTING_ANON_API={{$ST_ANON_API}}
78 |
79 | # Set the anonymous API quota per IP
80 | SETTING_ANON_API_QUOTA={{$ST_ANON_API_QUOTA}}
81 |
82 | # Set to true to use pseudorandom strings rather than using a counter by default
83 | SETTING_PSEUDORANDOM_ENDING={{$ST_PSEUDOR_ENDING}}
84 |
85 | # Set to true to record advanced analytics
86 | SETTING_ADV_ANALYTICS={{$ST_ADV_ANALYTICS}}
87 |
88 | # Set to true to restrict registration to a specific email domain
89 | SETTING_RESTRICT_EMAIL_DOMAIN={{$ST_RESTRICT_EMAIL_DOMAIN}}
90 |
91 | # A comma-separated list of permitted email domains
92 | SETTING_ALLOWED_EMAIL_DOMAINS="{{$ST_ALLOWED_EMAIL_DOMAINS}}"
93 |
94 | # reCAPTCHA site key
95 | POLR_RECAPTCHA_SITE_KEY="{{$POLR_RECAPTCHA_SITE_KEY}}"
96 |
97 | # reCAPTCHA secret key
98 | POLR_RECAPTCHA_SECRET_KEY="{{$POLR_RECAPTCHA_SECRET}}"
99 |
100 | # Set each to blank to disable mail
101 | @if($MAIL_ENABLED)
102 | MAIL_DRIVER=smtp
103 | # e.g mailtrap.io
104 | MAIL_HOST="{{$MAIL_HOST}}"
105 | # e.g 2525
106 | MAIL_PORT="{{$MAIL_PORT}}"
107 | MAIL_USERNAME="{{$MAIL_USERNAME}}"
108 | MAIL_PASSWORD="{{{$MAIL_PASSWORD}}}"
109 | # e.g noreply@example.com
110 | MAIL_FROM_ADDRESS="{{$MAIL_FROM_ADDRESS}}"
111 | MAIL_FROM_NAME="{{$MAIL_FROM_NAME}}"
112 | @endif
113 |
114 | APP_LOCALE=en
115 | APP_FALLBACK_LOCALE=en
116 |
117 | CACHE_DRIVER=file
118 | SESSION_DRIVER=file
119 | QUEUE_DRIVER=database
120 |
121 | _API_KEY_LENGTH=15
122 | _ANALYTICS_MAX_DAYS_DIFF=365
123 | _PSEUDO_RANDOM_KEY_LENGTH=5
124 |
125 | # FILESYSTEM_DRIVER=local
126 | # FILESYSTEM_CLOUD=s3
127 |
128 | # S3_KEY=null
129 | # S3_SECRET=null
130 | # S3_REGION=null
131 | # S3_BUCKET=null
132 |
133 | # RACKSPACE_USERNAME=null
134 | # RACKSPACE_KEY=null
135 | # RACKSPACE_CONTAINER=null
136 | # RACKSPACE_REGION=null
137 |
138 | # Set to 32 or 62 -- do not touch after initial configuration
139 | POLR_BASE={{$ST_BASE}}
140 |
141 | # Do not touch
142 | POLR_RELDATE="{{env('VERSION_RELMONTH')}} {{env('VERSION_RELDAY')}}, {{env('VERSION_RELYEAR')}}"
143 | POLR_VERSION="{{env('VERSION')}}"
144 | POLR_SECRET_BYTES=2
145 |
146 | TMP_SETUP_AUTH_KEY="{{$TMP_SETUP_AUTH_KEY}}"
147 |
--------------------------------------------------------------------------------
/app/Http/routes.php:
--------------------------------------------------------------------------------
1 | get('/signup', ['as' => 'signup', 'uses' => 'UserController@displaySignupPage']);
12 | $app->post('/signup', ['as' => 'psignup', 'uses' => 'UserController@performSignup']);
13 | }
14 |
15 | /* GET endpoints */
16 |
17 | $app->get('/', ['as' => 'index', 'uses' => 'IndexController@showIndexPage']);
18 | $app->get('/logout', ['as' => 'logout', 'uses' => 'UserController@performLogoutUser']);
19 | $app->get('/login', ['as' => 'login', 'uses' => 'UserController@displayLoginPage']);
20 | $app->get('/about-polr', ['as' => 'about', 'uses' => 'StaticPageController@displayAbout']);
21 |
22 | $app->get('/lost_password', ['as' => 'lost_password', 'uses' => 'UserController@displayLostPasswordPage']);
23 | $app->get('/activate/{username}/{recovery_key}', ['as' => 'activate', 'uses' => 'UserController@performActivation']);
24 | $app->get('/reset_password/{username}/{recovery_key}', ['as' => 'reset_password', 'uses' => 'UserController@performPasswordReset']);
25 |
26 | $app->get('/admin', ['as' => 'admin', 'uses' => 'AdminController@displayAdminPage']);
27 |
28 | $app->get('/setup', ['as' => 'setup', 'uses' => 'SetupController@displaySetupPage']);
29 | $app->post('/setup', ['as' => 'psetup', 'uses' => 'SetupController@performSetup']);
30 | $app->get('/setup/finish', ['as' => 'setup_finish', 'uses' => 'SetupController@finishSetup']);
31 |
32 | $app->get('/{short_url}', ['uses' => 'LinkController@performRedirect']);
33 | $app->get('/{short_url}/{secret_key}', ['uses' => 'LinkController@performRedirect']);
34 |
35 | $app->get('/admin/stats/{short_url}', ['uses' => 'StatsController@displayStats']);
36 |
37 | /* POST endpoints */
38 |
39 | $app->post('/login', ['as' => 'plogin', 'uses' => 'UserController@performLogin']);
40 | $app->post('/shorten', ['as' => 'pshorten', 'uses' => 'LinkController@performShorten']);
41 | $app->post('/lost_password', ['as' => 'plost_password', 'uses' => 'UserController@performSendPasswordResetCode']);
42 | $app->post('/reset_password/{username}/{recovery_key}', ['as' => 'preset_password', 'uses' => 'UserController@performPasswordReset']);
43 |
44 | $app->post('/admin/action/change_password', ['as' => 'change_password', 'uses' => 'AdminController@changePassword']);
45 |
46 | $app->group(['prefix' => '/api/v2', 'namespace' => 'App\Http\Controllers'], function ($app) {
47 | /* API internal endpoints */
48 | $app->post('link_avail_check', ['as' => 'api_link_check', 'uses' => 'AjaxController@checkLinkAvailability']);
49 | $app->post('admin/toggle_api_active', ['as' => 'api_toggle_api_active', 'uses' => 'AjaxController@toggleAPIActive']);
50 | $app->post('admin/generate_new_api_key', ['as' => 'api_generate_new_api_key', 'uses' => 'AjaxController@generateNewAPIKey']);
51 | $app->post('admin/edit_api_quota', ['as' => 'api_edit_quota', 'uses' => 'AjaxController@editAPIQuota']);
52 | $app->post('admin/toggle_user_active', ['as' => 'api_toggle_user_active', 'uses' => 'AjaxController@toggleUserActive']);
53 | $app->post('admin/change_user_role', ['as' => 'api_change_user_role', 'uses' => 'AjaxController@changeUserRole']);
54 | $app->post('admin/add_new_user', ['as' => 'api_add_new_user', 'uses' => 'AjaxController@addNewUser']);
55 | $app->post('admin/delete_user', ['as' => 'api_delete_user', 'uses' => 'AjaxController@deleteUser']);
56 | $app->post('admin/toggle_link', ['as' => 'api_toggle_link', 'uses' => 'AjaxController@toggleLink']);
57 | $app->post('admin/delete_link', ['as' => 'api_delete_link', 'uses' => 'AjaxController@deleteLink']);
58 | $app->post('admin/edit_link_long_url', ['as' => 'api_edit_link_long_url', 'uses' => 'AjaxController@editLinkLongUrl']);
59 |
60 | $app->get('admin/get_admin_users', ['as' => 'api_get_admin_users', 'uses' => 'AdminPaginationController@paginateAdminUsers']);
61 | $app->get('admin/get_admin_links', ['as' => 'api_get_admin_links', 'uses' => 'AdminPaginationController@paginateAdminLinks']);
62 | $app->get('admin/get_user_links', ['as' => 'api_get_user_links', 'uses' => 'AdminPaginationController@paginateUserLinks']);
63 | });
64 |
65 | $app->group(['prefix' => '/api/v2', 'namespace' => 'App\Http\Controllers\Api', 'middleware' => 'api'], function ($app) {
66 | /* API shorten endpoints */
67 | $app->post('action/shorten', ['as' => 'api_shorten_url', 'uses' => 'ApiLinkController@shortenLink']);
68 | $app->get('action/shorten', ['as' => 'api_shorten_url', 'uses' => 'ApiLinkController@shortenLink']);
69 |
70 | /* API lookup endpoints */
71 | $app->post('action/lookup', ['as' => 'api_lookup_url', 'uses' => 'ApiLinkController@lookupLink']);
72 | $app->get('action/lookup', ['as' => 'api_lookup_url', 'uses' => 'ApiLinkController@lookupLink']);
73 |
74 | /* API data endpoints */
75 | $app->get('data/link', ['as' => 'api_link_analytics', 'uses' => 'ApiAnalyticsController@lookupLinkStats']);
76 | $app->post('data/link', ['as' => 'api_link_analytics', 'uses' => 'ApiAnalyticsController@lookupLinkStats']);
77 | });
78 |
--------------------------------------------------------------------------------
/public/css/toastr.min.css:
--------------------------------------------------------------------------------
1 | .toast-title{font-weight:700}.toast-message{-ms-word-wrap:break-word;word-wrap:break-word}.toast-message a,.toast-message label{color:#fff}.toast-message a:hover{color:#ccc;text-decoration:none}.toast-close-button{position:relative;right:-.3em;top:-.3em;float:right;font-size:20px;font-weight:700;color:#fff;-webkit-text-shadow:0 1px 0 #fff;text-shadow:0 1px 0 #fff;opacity:.8;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80);filter:alpha(opacity=80)}.toast-close-button:focus,.toast-close-button:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.4;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=40);filter:alpha(opacity=40)}button.toast-close-button{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.toast-top-center{top:0;right:0;width:100%}.toast-bottom-center{bottom:0;right:0;width:100%}.toast-top-full-width{top:0;right:0;width:100%}.toast-bottom-full-width{bottom:0;right:0;width:100%}.toast-top-left{top:12px;left:12px}.toast-top-right{top:12px;right:12px}.toast-bottom-right{right:12px;bottom:12px}.toast-bottom-left{bottom:12px;left:12px}#toast-container{position:fixed;z-index:999999;pointer-events:none}#toast-container *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}#toast-container>div{position:relative;pointer-events:auto;overflow:hidden;margin:0 0 6px;padding:15px 15px 15px 50px;width:300px;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;background-position:15px center;background-repeat:no-repeat;-moz-box-shadow:0 0 12px #999;-webkit-box-shadow:0 0 12px #999;box-shadow:0 0 12px #999;color:#fff;opacity:.8;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80);filter:alpha(opacity=80)}#toast-container>:hover{-moz-box-shadow:0 0 12px #000;-webkit-box-shadow:0 0 12px #000;box-shadow:0 0 12px #000;opacity:1;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=100);filter:alpha(opacity=100);cursor:pointer}#toast-container>.toast-info{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII=)!important}#toast-container>.toast-error{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=)!important}#toast-container>.toast-success{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==)!important}#toast-container>.toast-warning{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=)!important}#toast-container.toast-bottom-center>div,#toast-container.toast-top-center>div{width:300px;margin-left:auto;margin-right:auto}#toast-container.toast-bottom-full-width>div,#toast-container.toast-top-full-width>div{width:96%;margin-left:auto;margin-right:auto}.toast{background-color:#030303}.toast-success{background-color:#51a351}.toast-error{background-color:#bd362f}.toast-info{background-color:#2f96b4}.toast-warning{background-color:#f89406}.toast-progress{position:absolute;left:0;bottom:0;height:4px;background-color:#000;opacity:.4;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=40);filter:alpha(opacity=40)}@media all and (max-width:240px){#toast-container>div{padding:8px 8px 8px 50px;width:11em}#toast-container .toast-close-button{right:-.2em;top:-.2em}}@media all and (min-width:241px) and (max-width:480px){#toast-container>div{padding:8px 8px 8px 50px;width:18em}#toast-container .toast-close-button{right:-.2em;top:-.2em}}@media all and (min-width:481px) and (max-width:768px){#toast-container>div{padding:15px 15px 15px 50px;width:25em}}
--------------------------------------------------------------------------------
/public/js/StatsCtrl.js:
--------------------------------------------------------------------------------
1 | var parseInputDate = function (inputDate) {
2 | return moment(inputDate);
3 | };
4 |
5 | polr.controller('StatsCtrl', function($scope, $compile) {
6 | $scope.dayChart = null;
7 | $scope.refererChart = null;
8 | $scope.countryChart = null;
9 |
10 | $scope.dayData = dayData;
11 | $scope.refererData = refererData;
12 | $scope.countryData = countryData;
13 |
14 | $scope.populateEmptyDayData = function () {
15 | // Populate empty days in $scope.dayData with zeroes
16 |
17 | // Number of days in range
18 | var numDays = moment(datePickerRightBound).diff(moment(datePickerLeftBound), 'days');
19 | var i = moment(datePickerLeftBound);
20 |
21 | var daysWithData = {};
22 |
23 | // Generate hash map to keep track of dates with data
24 | _.each($scope.dayData, function (point) {
25 | var dayDate = point.x;
26 | daysWithData[dayDate] = true;
27 | });
28 |
29 | // Push zeroes for days without data
30 | _.each(_.range(0, numDays), function () {
31 | var formattedDate = i.format('YYYY-MM-DD');
32 |
33 | if (!(formattedDate in daysWithData)) {
34 | // If day does not have data, fill in with 0
35 | $scope.dayData.push({
36 | x: formattedDate,
37 | y: 0
38 | })
39 | }
40 |
41 | i.add(1, 'day');
42 | });
43 |
44 | // Sort dayData from least to most recent
45 | // to ensure Chart.js displays the data correctly
46 | $scope.dayData = _.sortBy($scope.dayData, ['x'])
47 | }
48 |
49 | $scope.initDayChart = function () {
50 | var ctx = $("#dayChart");
51 |
52 | // Populate empty days in dayData
53 | $scope.populateEmptyDayData();
54 |
55 | $scope.dayChart = new Chart(ctx, {
56 | type: 'line',
57 | data: {
58 | datasets: [{
59 | label: 'Clicks',
60 | data: $scope.dayData,
61 | pointHoverBackgroundColor: "rgba(75,192,192,1)",
62 | pointHoverBorderColor: "rgba(220,220,220,1)",
63 | backgroundColor: "rgba(75,192,192,0.4)",
64 | borderColor: "rgba(75,192,192,1)",
65 | }]
66 | },
67 | options: {
68 | scales: {
69 | xAxes: [{
70 | type: 'time',
71 | time: {
72 | unit: 'day'
73 | }
74 | }],
75 | yAxes: [{
76 | ticks: {
77 | min: 0
78 | }
79 | }]
80 | }
81 | }
82 | });
83 | };
84 | $scope.initRefererChart = function () {
85 | // Traffic sources
86 | var ctx = $("#refererChart");
87 |
88 | var srcLabels = [];
89 | // var bgColors = [];
90 | var bgColors = [ '#003559', '#162955', '#2E4272', '#4F628E', '#7887AB', '#b9d6f2'];
91 | var srcData = [];
92 |
93 | _.each($scope.refererData, function (item) {
94 | if (srcLabels.length > 6) {
95 | // If more than 6 referers are listed, push the seventh and
96 | // beyond into "other"
97 | srcLabels[6] = 'Other';
98 | srcData[6] += item.clicks;
99 | bgColors[6] = 'brown';
100 | return;
101 | }
102 |
103 | srcLabels.push(item.label);
104 | srcData.push(item.clicks);
105 | });
106 |
107 | $scope.refererChart = new Chart(ctx, {
108 | type: 'pie',
109 | data: {
110 | labels: srcLabels,
111 | datasets: [{
112 | data: srcData,
113 | backgroundColor: bgColors
114 | }]
115 | }
116 | });
117 |
118 | $('#refererTable').DataTable();
119 | };
120 | $scope.initCountryChart = function () {
121 | var parsedCountryData = {};
122 |
123 | _.each($scope.countryData, function(country) {
124 | parsedCountryData[country.label] = country.clicks;
125 | });
126 |
127 | $('#mapChart').vectorMap({
128 | map: 'world_mill',
129 | series: {
130 | regions: [{
131 | values: parsedCountryData,
132 | scale: ['#C8EEFF', '#0071A4'],
133 | normalizeFunction: 'polynomial'
134 | }]
135 | },
136 | onRegionTipShow: function(e, el, code) {
137 | el.html(el.html()+' (' + (parsedCountryData[code] || 0) + ')');
138 | }
139 | });
140 |
141 | };
142 |
143 | $scope.initDatePickers = function () {
144 | var $leftPicker = $('#left-bound-picker');
145 | var $rightPicker = $('#right-bound-picker');
146 |
147 | var datePickerOptions = {
148 | showTodayButton: true
149 | }
150 |
151 | $leftPicker.datetimepicker(datePickerOptions);
152 | $rightPicker.datetimepicker(datePickerOptions);
153 |
154 | $leftPicker.data("DateTimePicker").parseInputDate(parseInputDate);
155 | $rightPicker.data("DateTimePicker").parseInputDate(parseInputDate);
156 |
157 | $leftPicker.data("DateTimePicker").date(datePickerLeftBound, Date, moment, null);
158 | $rightPicker.data("DateTimePicker").date(datePickerRightBound, Date, moment, null);
159 | }
160 |
161 | $scope.init = function () {
162 | $scope.initDayChart();
163 | $scope.initRefererChart();
164 | $scope.initCountryChart();
165 | $scope.initDatePickers();
166 | };
167 |
168 | $scope.init();
169 |
170 | });
171 |
--------------------------------------------------------------------------------
/resources/lang/en/validation.php:
--------------------------------------------------------------------------------
1 | 'The :attribute must be accepted.',
17 | 'active_url' => 'The :attribute is not a valid URL.',
18 | 'after' => 'The :attribute must be a date after :date.',
19 | 'alpha' => 'The :attribute may only contain letters.',
20 | 'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.',
21 | 'alpha_num' => 'The :attribute may only contain letters and numbers.',
22 | 'array' => 'The :attribute must be an array.',
23 | 'before' => 'The :attribute must be a date before :date.',
24 | 'between' => [
25 | 'numeric' => 'The :attribute must be between :min and :max.',
26 | 'file' => 'The :attribute must be between :min and :max kilobytes.',
27 | 'string' => 'The :attribute must be between :min and :max characters.',
28 | 'array' => 'The :attribute must have between :min and :max items.',
29 | ],
30 | 'boolean' => 'The :attribute field must be true or false.',
31 | 'confirmed' => 'The :attribute confirmation does not match.',
32 | 'date' => 'The :attribute is not a valid date.',
33 | 'date_format' => 'The :attribute does not match the format :format.',
34 | 'different' => 'The :attribute and :other must be different.',
35 | 'digits' => 'The :attribute must be :digits digits.',
36 | 'digits_between' => 'The :attribute must be between :min and :max digits.',
37 | 'email' => 'The :attribute must be a valid email address.',
38 | 'filled' => 'The :attribute field is required.',
39 | 'exists' => 'The selected :attribute is invalid.',
40 | 'image' => 'The :attribute must be an image.',
41 | 'in' => 'The selected :attribute is invalid.',
42 | 'integer' => 'The :attribute must be an integer.',
43 | 'ip' => 'The :attribute must be a valid IP address.',
44 | 'max' => [
45 | 'numeric' => 'The :attribute may not be greater than :max.',
46 | 'file' => 'The :attribute may not be greater than :max kilobytes.',
47 | 'string' => 'The :attribute may not be greater than :max characters.',
48 | 'array' => 'The :attribute may not have more than :max items.',
49 | ],
50 | 'mimes' => 'The :attribute must be a file of type: :values.',
51 | 'min' => [
52 | 'numeric' => 'The :attribute must be at least :min.',
53 | 'file' => 'The :attribute must be at least :min kilobytes.',
54 | 'string' => 'The :attribute must be at least :min characters.',
55 | 'array' => 'The :attribute must have at least :min items.',
56 | ],
57 | 'not_in' => 'The selected :attribute is invalid.',
58 | 'numeric' => 'The :attribute must be a number.',
59 | 'regex' => 'The :attribute format is invalid.',
60 | 'required' => 'The :attribute field is required.',
61 | 'required_if' => 'The :attribute field is required when :other is :value.',
62 | 'required_with' => 'The :attribute field is required when :values is present.',
63 | 'required_with_all' => 'The :attribute field is required when :values is present.',
64 | 'required_without' => 'The :attribute field is required when :values is not present.',
65 | 'required_without_all' => 'The :attribute field is required when none of :values are present.',
66 | 'same' => 'The :attribute and :other must match.',
67 | 'size' => [
68 | 'numeric' => 'The :attribute must be :size.',
69 | 'file' => 'The :attribute must be :size kilobytes.',
70 | 'string' => 'The :attribute must be :size characters.',
71 | 'array' => 'The :attribute must contain :size items.',
72 | ],
73 | 'unique' => 'The :attribute has already been taken.',
74 | 'url' => 'The :attribute format is invalid.',
75 | 'timezone' => 'The :attribute must be a valid zone.',
76 |
77 | /*
78 | |--------------------------------------------------------------------------
79 | | Custom Validation Language Lines
80 | |--------------------------------------------------------------------------
81 | |
82 | | Here you may specify custom validation messages for attributes using the
83 | | convention "attribute.rule" to name the lines. This makes it quick to
84 | | specify a specific custom language line for a given attribute rule.
85 | |
86 | */
87 |
88 | 'custom' => [
89 | 'attribute-name' => [
90 | 'rule-name' => 'custom-message',
91 | ],
92 | ],
93 |
94 | /*
95 | |--------------------------------------------------------------------------
96 | | Custom Validation Attributes
97 | |--------------------------------------------------------------------------
98 | |
99 | | The following language lines are used to swap attribute place-holders
100 | | with something more reader friendly such as E-Mail Address instead
101 | | of "email". This simply helps us make messages a little cleaner.
102 | |
103 | */
104 |
105 | 'attributes' => [
106 | 'link-url' => 'link URL'
107 | ],
108 |
109 | ];
110 |
--------------------------------------------------------------------------------
/resources/lang/zh/validation.php:
--------------------------------------------------------------------------------
1 | 'The :attribute must be accepted.',
17 | 'active_url' => 'The :attribute is not a valid URL.',
18 | 'after' => 'The :attribute must be a date after :date.',
19 | 'alpha' => 'The :attribute may only contain letters.',
20 | 'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.',
21 | 'alpha_num' => 'The :attribute may only contain letters and numbers.',
22 | 'array' => 'The :attribute must be an array.',
23 | 'before' => 'The :attribute must be a date before :date.',
24 | 'between' => [
25 | 'numeric' => 'The :attribute must be between :min and :max.',
26 | 'file' => 'The :attribute must be between :min and :max kilobytes.',
27 | 'string' => 'The :attribute must be between :min and :max characters.',
28 | 'array' => 'The :attribute must have between :min and :max items.',
29 | ],
30 | 'boolean' => 'The :attribute field must be true or false.',
31 | 'confirmed' => 'The :attribute confirmation does not match.',
32 | 'date' => 'The :attribute is not a valid date.',
33 | 'date_format' => 'The :attribute does not match the format :format.',
34 | 'different' => 'The :attribute and :other must be different.',
35 | 'digits' => 'The :attribute must be :digits digits.',
36 | 'digits_between' => 'The :attribute must be between :min and :max digits.',
37 | 'email' => 'The :attribute must be a valid email address.',
38 | 'filled' => 'The :attribute field is required.',
39 | 'exists' => 'The selected :attribute is invalid.',
40 | 'image' => 'The :attribute must be an image.',
41 | 'in' => 'The selected :attribute is invalid.',
42 | 'integer' => 'The :attribute must be an integer.',
43 | 'ip' => 'The :attribute must be a valid IP address.',
44 | 'max' => [
45 | 'numeric' => 'The :attribute may not be greater than :max.',
46 | 'file' => 'The :attribute may not be greater than :max kilobytes.',
47 | 'string' => 'The :attribute may not be greater than :max characters.',
48 | 'array' => 'The :attribute may not have more than :max items.',
49 | ],
50 | 'mimes' => 'The :attribute must be a file of type: :values.',
51 | 'min' => [
52 | 'numeric' => 'The :attribute must be at least :min.',
53 | 'file' => 'The :attribute must be at least :min kilobytes.',
54 | 'string' => 'The :attribute must be at least :min characters.',
55 | 'array' => 'The :attribute must have at least :min items.',
56 | ],
57 | 'not_in' => 'The selected :attribute is invalid.',
58 | 'numeric' => 'The :attribute must be a number.',
59 | 'regex' => 'The :attribute format is invalid.',
60 | 'required' => 'The :attribute field is required.',
61 | 'required_if' => 'The :attribute field is required when :other is :value.',
62 | 'required_with' => 'The :attribute field is required when :values is present.',
63 | 'required_with_all' => 'The :attribute field is required when :values is present.',
64 | 'required_without' => 'The :attribute field is required when :values is not present.',
65 | 'required_without_all' => 'The :attribute field is required when none of :values are present.',
66 | 'same' => 'The :attribute and :other must match.',
67 | 'size' => [
68 | 'numeric' => 'The :attribute must be :size.',
69 | 'file' => 'The :attribute must be :size kilobytes.',
70 | 'string' => 'The :attribute must be :size characters.',
71 | 'array' => 'The :attribute must contain :size items.',
72 | ],
73 | 'unique' => 'The :attribute has already been taken.',
74 | 'url' => 'The :attribute format is invalid.',
75 | 'timezone' => 'The :attribute must be a valid zone.',
76 |
77 | /*
78 | |--------------------------------------------------------------------------
79 | | Custom Validation Language Lines
80 | |--------------------------------------------------------------------------
81 | |
82 | | Here you may specify custom validation messages for attributes using the
83 | | convention "attribute.rule" to name the lines. This makes it quick to
84 | | specify a specific custom language line for a given attribute rule.
85 | |
86 | */
87 |
88 | 'custom' => [
89 | 'attribute-name' => [
90 | 'rule-name' => 'custom-message',
91 | ],
92 | ],
93 |
94 | /*
95 | |--------------------------------------------------------------------------
96 | | Custom Validation Attributes
97 | |--------------------------------------------------------------------------
98 | |
99 | | The following language lines are used to swap attribute place-holders
100 | | with something more reader friendly such as E-Mail Address instead
101 | | of "email". This simply helps us make messages a little cleaner.
102 | |
103 | */
104 |
105 | 'attributes' => [
106 | 'link-url' => 'link URL'
107 | ],
108 |
109 | ];
110 |
--------------------------------------------------------------------------------
/resources/views/admin.blade.php:
--------------------------------------------------------------------------------
1 | @extends('layouts.base')
2 |
3 | @section('css')
4 |
5 |
6 | @endsection
7 |
8 | @section('content')
9 |
10 |
11 |
12 | 后台首页
13 | 短链接
14 | 设 置
15 |
16 | @if ($role == $admin_role)
17 | 管理员
18 | @endif
19 |
20 | @if ($api_active == 1)
21 | 开发者
22 | @endif
23 |
24 |
25 |
26 |
27 |
28 |
欢迎光临!
29 |
这里是后台首页,左边有导航,右上角是用户中心。
30 |
31 |
32 |
33 | @include('snippets.link_table', [
34 | 'table_id' => 'user_links_table'
35 | ])
36 |
37 |
38 |
39 |
修改密码
40 |
46 |
47 |
48 | @if ($role == $admin_role)
49 |
50 |
链接
51 | @include('snippets.link_table', [
52 | 'table_id' => 'admin_links_table'
53 | ])
54 |
55 |
用 户
56 |
添加用户
57 |
58 |
84 |
85 | @include('snippets.user_table', [
86 | 'table_id' => 'admin_users_table'
87 | ])
88 |
89 |
90 | @endif
91 |
92 | @if ($api_active == 1)
93 |
94 |
开发者
95 |
96 |
API keys and documentation for developers.
97 |
98 | Documentation:
99 | http://docs.polr.me/en/latest/developer-guide/api/
100 |
101 |
102 |
API Key:
103 |
104 |
105 |
106 |
107 |
110 |
111 |
112 |
113 |
API Quota:
114 |
115 | @if ($api_quota == -1)
116 | unlimited
117 | @else
118 | {{$api_quota}}
119 | @endif
120 |
121 |
requests per minute
122 |
123 | @endif
124 |
125 |
126 |
127 |
128 |
130 |
133 |
134 |
135 |
136 |
137 | @endsection
138 |
139 | @section('js')
140 | {{-- Include modal templates --}}
141 | @include('snippets.modals')
142 |
143 | {{-- Include extra JS --}}
144 |
145 |
146 |
147 | @endsection
148 |
--------------------------------------------------------------------------------
/app/Http/Controllers/AdminPaginationController.php:
--------------------------------------------------------------------------------
1 | long_url) . '" href="'. $link->long_url .'">' . str_limit($link->long_url, 50) . '
21 |
';
22 | }
23 |
24 | public function renderClicksCell($link) {
25 | if (env('SETTING_ADV_ANALYTICS')) {
26 | return $link->clicks . '
27 |
28 | ';
29 | }
30 | else {
31 | return $link->clicks;
32 | }
33 | }
34 |
35 | public function renderDeleteUserCell($user) {
36 | // Add "Delete" action button
37 | $btn_class = '';
38 | if (session('username') === $user->username) {
39 | $btn_class = 'disabled';
40 | }
41 | return '
42 | Delete
43 | ';
44 | }
45 |
46 | public function renderDeleteLinkCell($link) {
47 | // Add "Delete" action button
48 | return '
50 | Delete
51 | ';
52 | }
53 |
54 | public function renderAdminApiActionCell($user) {
55 | // Add "API Info" action button
56 | return '
58 | API info
59 | ';
60 | }
61 |
62 | public function renderToggleUserActiveCell($user) {
63 | // Add user account active state toggle buttons
64 | $btn_class = '';
65 | if (session('username') === $user->username) {
66 | $btn_class = ' disabled';
67 | }
68 |
69 | if ($user->active) {
70 | $active_text = 'Active';
71 | $btn_color_class = ' btn-success';
72 | }
73 | else {
74 | $active_text = 'Inactive';
75 | $btn_color_class = ' btn-danger';
76 | }
77 |
78 | return '
' . $active_text . ' ';
79 | }
80 |
81 | public function renderChangeUserRoleCell($user) {
82 | // Add "change role" select box
83 | //
field does not use Angular bindings
84 | // because of an issue affecting fields with duplicate names.
85 |
86 | $select_role = 'username) {
91 | // Do not allow user to change own role
92 | $select_role .= ' disabled';
93 | }
94 | $select_role .= '>';
95 |
96 | foreach (UserHelper::$USER_ROLES as $role_text => $role_val) {
97 | // Iterate over each available role and output option
98 | $select_role .= 'role === $role_val) {
101 | $select_role .= ' selected';
102 | }
103 |
104 | $select_role .= '>' . e($role_text) . ' ';
105 | }
106 |
107 | $select_role .= ' ';
108 | return $select_role;
109 | }
110 |
111 | public function renderToggleLinkActiveCell($link) {
112 | // Add "Disable/Enable" action buttons
113 | $btn_class = 'btn-danger';
114 | $btn_text = 'Disable';
115 |
116 | if ($link->is_disabled) {
117 | $btn_class = 'btn-success';
118 | $btn_text = 'Enable';
119 | }
120 |
121 | return '
122 | ' . $btn_text . '
123 | ';
124 | }
125 |
126 | /* DataTables bindings */
127 |
128 | public function paginateAdminUsers(Request $request) {
129 | self::ensureAdmin();
130 |
131 | $admin_users = User::select(['username', 'email', 'created_at', 'active', 'api_key', 'api_active', 'api_quota', 'role', 'id']);
132 | return Datatables::of($admin_users)
133 | ->addColumn('api_action', [$this, 'renderAdminApiActionCell'])
134 | ->addColumn('toggle_active', [$this, 'renderToggleUserActiveCell'])
135 | ->addColumn('change_role', [$this, 'renderChangeUserRoleCell'])
136 | ->addColumn('delete', [$this, 'renderDeleteUserCell'])
137 | ->escapeColumns(['username', 'email'])
138 | ->make(true);
139 | }
140 |
141 | public function paginateAdminLinks(Request $request) {
142 | self::ensureAdmin();
143 |
144 | $admin_links = Link::select(['short_url', 'long_url', 'clicks', 'created_at', 'creator', 'is_disabled']);
145 | return Datatables::of($admin_links)
146 | ->addColumn('disable', [$this, 'renderToggleLinkActiveCell'])
147 | ->addColumn('delete', [$this, 'renderDeleteLinkCell'])
148 | ->editColumn('clicks', [$this, 'renderClicksCell'])
149 | ->editColumn('long_url', [$this, 'renderLongUrlCell'])
150 | ->escapeColumns(['short_url', 'creator'])
151 | ->make(true);
152 | }
153 |
154 | public function paginateUserLinks(Request $request) {
155 | self::ensureLoggedIn();
156 |
157 | $username = session('username');
158 | $user_links = Link::where('creator', $username)
159 | ->select(['id', 'short_url', 'long_url', 'clicks', 'created_at']);
160 |
161 | return Datatables::of($user_links)
162 | ->editColumn('clicks', [$this, 'renderClicksCell'])
163 | ->editColumn('long_url', [$this, 'renderLongUrlCell'])
164 | ->escapeColumns(['short_url'])
165 | ->make(true);
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/public/css/jquery-jvectormap.css:
--------------------------------------------------------------------------------
1 | svg {
2 | touch-action: none;
3 | }
4 |
5 | .jvectormap-container {
6 | width: 100%;
7 | height: 100%;
8 | position: relative;
9 | overflow: hidden;
10 | touch-action: none;
11 | }
12 |
13 | .jvectormap-tip {
14 | position: absolute;
15 | display: none;
16 | border: solid 1px #CDCDCD;
17 | border-radius: 3px;
18 | background: #292929;
19 | color: white;
20 | font-family: sans-serif, Verdana;
21 | font-size: smaller;
22 | padding: 3px;
23 | }
24 |
25 | .jvectormap-zoomin, .jvectormap-zoomout, .jvectormap-goback {
26 | position: absolute;
27 | left: 10px;
28 | border-radius: 3px;
29 | background: #292929;
30 | padding: 3px;
31 | color: white;
32 | cursor: pointer;
33 | line-height: 10px;
34 | text-align: center;
35 | box-sizing: content-box;
36 | }
37 |
38 | .jvectormap-zoomin, .jvectormap-zoomout {
39 | width: 10px;
40 | height: 10px;
41 | }
42 |
43 | .jvectormap-zoomin {
44 | top: 10px;
45 | }
46 |
47 | .jvectormap-zoomout {
48 | top: 30px;
49 | }
50 |
51 | .jvectormap-goback {
52 | bottom: 10px;
53 | z-index: 1000;
54 | padding: 6px;
55 | }
56 |
57 | .jvectormap-spinner {
58 | position: absolute;
59 | left: 0;
60 | top: 0;
61 | right: 0;
62 | bottom: 0;
63 | background: center no-repeat url(data:image/gif;base64,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);
64 | }
65 |
66 | .jvectormap-legend-title {
67 | font-weight: bold;
68 | font-size: 14px;
69 | text-align: center;
70 | }
71 |
72 | .jvectormap-legend-cnt {
73 | position: absolute;
74 | }
75 |
76 | .jvectormap-legend-cnt-h {
77 | bottom: 0;
78 | right: 0;
79 | }
80 |
81 | .jvectormap-legend-cnt-v {
82 | top: 0;
83 | right: 0;
84 | }
85 |
86 | .jvectormap-legend {
87 | background: black;
88 | color: white;
89 | border-radius: 3px;
90 | }
91 |
92 | .jvectormap-legend-cnt-h .jvectormap-legend {
93 | float: left;
94 | margin: 0 10px 10px 0;
95 | padding: 3px 3px 1px 3px;
96 | }
97 |
98 | .jvectormap-legend-cnt-h .jvectormap-legend .jvectormap-legend-tick {
99 | float: left;
100 | }
101 |
102 | .jvectormap-legend-cnt-v .jvectormap-legend {
103 | margin: 10px 10px 0 0;
104 | padding: 3px;
105 | }
106 |
107 | .jvectormap-legend-cnt-h .jvectormap-legend-tick {
108 | width: 40px;
109 | }
110 |
111 | .jvectormap-legend-cnt-h .jvectormap-legend-tick-sample {
112 | height: 15px;
113 | }
114 |
115 | .jvectormap-legend-cnt-v .jvectormap-legend-tick-sample {
116 | height: 20px;
117 | width: 20px;
118 | display: inline-block;
119 | vertical-align: middle;
120 | }
121 |
122 | .jvectormap-legend-tick-text {
123 | font-size: 12px;
124 | }
125 |
126 | .jvectormap-legend-cnt-h .jvectormap-legend-tick-text {
127 | text-align: center;
128 | }
129 |
130 | .jvectormap-legend-cnt-v .jvectormap-legend-tick-text {
131 | display: inline-block;
132 | vertical-align: middle;
133 | line-height: 20px;
134 | padding-left: 3px;
135 | }
--------------------------------------------------------------------------------
/public/css/bootstrap-datetimepicker.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Datetimepicker for Bootstrap 3
3 | * version : 4.17.47
4 | * https://github.com/Eonasdan/bootstrap-datetimepicker/
5 | */.bootstrap-datetimepicker-widget{list-style:none}.bootstrap-datetimepicker-widget.dropdown-menu{display:block;margin:2px 0;padding:4px;width:19em}@media (min-width:768px){.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs{width:38em}}@media (min-width:992px){.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs{width:38em}}@media (min-width:1200px){.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs{width:38em}}.bootstrap-datetimepicker-widget.dropdown-menu:before,.bootstrap-datetimepicker-widget.dropdown-menu:after{content:'';display:inline-block;position:absolute}.bootstrap-datetimepicker-widget.dropdown-menu.bottom:before{border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-bottom-color:rgba(0,0,0,0.2);top:-7px;left:7px}.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after{border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid white;top:-6px;left:8px}.bootstrap-datetimepicker-widget.dropdown-menu.top:before{border-left:7px solid transparent;border-right:7px solid transparent;border-top:7px solid #ccc;border-top-color:rgba(0,0,0,0.2);bottom:-7px;left:6px}.bootstrap-datetimepicker-widget.dropdown-menu.top:after{border-left:6px solid transparent;border-right:6px solid transparent;border-top:6px solid white;bottom:-6px;left:7px}.bootstrap-datetimepicker-widget.dropdown-menu.pull-right:before{left:auto;right:6px}.bootstrap-datetimepicker-widget.dropdown-menu.pull-right:after{left:auto;right:7px}.bootstrap-datetimepicker-widget .list-unstyled{margin:0}.bootstrap-datetimepicker-widget a[data-action]{padding:6px 0}.bootstrap-datetimepicker-widget a[data-action]:active{box-shadow:none}.bootstrap-datetimepicker-widget .timepicker-hour,.bootstrap-datetimepicker-widget .timepicker-minute,.bootstrap-datetimepicker-widget .timepicker-second{width:54px;font-weight:bold;font-size:1.2em;margin:0}.bootstrap-datetimepicker-widget button[data-action]{padding:6px}.bootstrap-datetimepicker-widget .btn[data-action="incrementHours"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Increment Hours"}.bootstrap-datetimepicker-widget .btn[data-action="incrementMinutes"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Increment Minutes"}.bootstrap-datetimepicker-widget .btn[data-action="decrementHours"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Decrement Hours"}.bootstrap-datetimepicker-widget .btn[data-action="decrementMinutes"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Decrement Minutes"}.bootstrap-datetimepicker-widget .btn[data-action="showHours"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Show Hours"}.bootstrap-datetimepicker-widget .btn[data-action="showMinutes"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Show Minutes"}.bootstrap-datetimepicker-widget .btn[data-action="togglePeriod"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Toggle AM/PM"}.bootstrap-datetimepicker-widget .btn[data-action="clear"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Clear the picker"}.bootstrap-datetimepicker-widget .btn[data-action="today"]::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Set the date to today"}.bootstrap-datetimepicker-widget .picker-switch{text-align:center}.bootstrap-datetimepicker-widget .picker-switch::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Toggle Date and Time Screens"}.bootstrap-datetimepicker-widget .picker-switch td{padding:0;margin:0;height:auto;width:auto;line-height:inherit}.bootstrap-datetimepicker-widget .picker-switch td span{line-height:2.5;height:2.5em;width:100%}.bootstrap-datetimepicker-widget table{width:100%;margin:0}.bootstrap-datetimepicker-widget table td,.bootstrap-datetimepicker-widget table th{text-align:center;border-radius:4px}.bootstrap-datetimepicker-widget table th{height:20px;line-height:20px;width:20px}.bootstrap-datetimepicker-widget table th.picker-switch{width:145px}.bootstrap-datetimepicker-widget table th.disabled,.bootstrap-datetimepicker-widget table th.disabled:hover{background:none;color:#777;cursor:not-allowed}.bootstrap-datetimepicker-widget table th.prev::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Previous Month"}.bootstrap-datetimepicker-widget table th.next::after{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;content:"Next Month"}.bootstrap-datetimepicker-widget table thead tr:first-child th{cursor:pointer}.bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#eee}.bootstrap-datetimepicker-widget table td{height:54px;line-height:54px;width:54px}.bootstrap-datetimepicker-widget table td.cw{font-size:.8em;height:20px;line-height:20px;color:#777}.bootstrap-datetimepicker-widget table td.day{height:20px;line-height:20px;width:20px}.bootstrap-datetimepicker-widget table td.day:hover,.bootstrap-datetimepicker-widget table td.hour:hover,.bootstrap-datetimepicker-widget table td.minute:hover,.bootstrap-datetimepicker-widget table td.second:hover{background:#eee;cursor:pointer}.bootstrap-datetimepicker-widget table td.old,.bootstrap-datetimepicker-widget table td.new{color:#777}.bootstrap-datetimepicker-widget table td.today{position:relative}.bootstrap-datetimepicker-widget table td.today:before{content:'';display:inline-block;border:solid transparent;border-width:0 0 7px 7px;border-bottom-color:#337ab7;border-top-color:rgba(0,0,0,0.2);position:absolute;bottom:4px;right:4px}.bootstrap-datetimepicker-widget table td.active,.bootstrap-datetimepicker-widget table td.active:hover{background-color:#337ab7;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.bootstrap-datetimepicker-widget table td.active.today:before{border-bottom-color:#fff}.bootstrap-datetimepicker-widget table td.disabled,.bootstrap-datetimepicker-widget table td.disabled:hover{background:none;color:#777;cursor:not-allowed}.bootstrap-datetimepicker-widget table td span{display:inline-block;width:54px;height:54px;line-height:54px;margin:2px 1.5px;cursor:pointer;border-radius:4px}.bootstrap-datetimepicker-widget table td span:hover{background:#eee}.bootstrap-datetimepicker-widget table td span.active{background-color:#337ab7;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.bootstrap-datetimepicker-widget table td span.old{color:#777}.bootstrap-datetimepicker-widget table td span.disabled,.bootstrap-datetimepicker-widget table td span.disabled:hover{background:none;color:#777;cursor:not-allowed}.bootstrap-datetimepicker-widget.usetwentyfour td.hour{height:27px;line-height:27px}.bootstrap-datetimepicker-widget.wider{width:21em}.bootstrap-datetimepicker-widget .datepicker-decades .decade{line-height:1.8em !important}.input-group.date .input-group-addon{cursor:pointer}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}
--------------------------------------------------------------------------------