diff --git a/app/Album.php b/app/Album.php index 8e94eda..d19c77b 100644 --- a/app/Album.php +++ b/app/Album.php @@ -123,6 +123,12 @@ class Album extends Model } } + if (is_null($current->parent_album_id) && $current->is_permissions_inherited) + { + // Use default permissions list + return 0; + } + return $current->id; } diff --git a/app/AlbumDefaultAnonymousPermission.php b/app/AlbumDefaultAnonymousPermission.php new file mode 100644 index 0000000..6924144 --- /dev/null +++ b/app/AlbumDefaultAnonymousPermission.php @@ -0,0 +1,9 @@ + false, 'albums_menu_number_items' => 10, + 'allow_photo_comments' => false, + 'allow_photo_comments_anonymous' => true, 'allow_self_registration' => true, 'analytics_code' => '', 'app_name' => trans('global.app_name'), @@ -111,6 +113,10 @@ class ConfigHelper 'hotlink_protection' => false, 'items_per_page' => 12, 'items_per_page_admin' => 10, + 'moderate_anonymous_users' => true, + 'moderate_known_users' => true, + 'photo_comments_allowed_html' => 'p,div,span,a,b,i,u', + 'photo_comments_thread_depth' => 3, 'public_statistics' => true, 'recaptcha_enabled_registration' => false, 'recaptcha_secret_key' => '', diff --git a/app/Helpers/PermissionsHelper.php b/app/Helpers/PermissionsHelper.php index 1e3d626..de42934 100644 --- a/app/Helpers/PermissionsHelper.php +++ b/app/Helpers/PermissionsHelper.php @@ -3,6 +3,9 @@ namespace App\Helpers; use App\Album; +use App\AlbumDefaultAnonymousPermission; +use App\AlbumDefaultGroupPermission; +use App\AlbumDefaultUserPermission; use App\Permission; use App\User; use Illuminate\Support\Facades\Auth; @@ -50,6 +53,23 @@ class PermissionsHelper ->count() > 0; } + public function usersWhoCan_Album(Album $album, $permission) + { + $users = DB::table('album_permissions_cache') + ->join('permissions', 'permissions.id', '=', 'album_permissions_cache.permission_id') + ->where([ + ['album_permissions_cache.album_id', $album->id], + ['permissions.section', 'album'], + ['permissions.description', $permission] + ]) + ->get(); + + // Include the album's owner (who can do everything) + $users->push($album->user); + + return $users; + } + private function rebuildAlbumCache() { // Get a list of albums @@ -60,6 +80,10 @@ class PermissionsHelper $albumGroupPermissions = DB::table('album_group_permissions')->get(); $albumAnonPermissions = DB::table('album_anonymous_permissions')->get(); + $defaultAlbumUserPermissions = AlbumDefaultUserPermission::all(); + $defaultAlbumGroupPermissions = AlbumDefaultGroupPermission::all(); + $defaultAnonPermissions = AlbumDefaultAnonymousPermission::all(); + // Get a list of all user->group memberships $userGroups = DB::table('user_groups')->get(); @@ -71,60 +95,109 @@ class PermissionsHelper { $effectiveAlbumID = $album->effectiveAlbumIDForPermissions(); - $anonymousPermissions = array_filter($albumAnonPermissions->toArray(), function($item) use ($effectiveAlbumID) + if ($effectiveAlbumID === 0) { - return ($item->album_id == $effectiveAlbumID); - }); + /* Use the default permissions list */ - foreach ($anonymousPermissions as $anonymousPermission) - { - $permissionsCache[] = [ - 'album_id' => $album->id, - 'permission_id' => $anonymousPermission->permission_id, - 'created_at' => new \DateTime(), - 'updated_at' => new \DateTime() - ]; - } - - $userPermissions = array_filter($albumUserPermissions->toArray(), function($item) use ($effectiveAlbumID) - { - return ($item->album_id == $effectiveAlbumID); - }); - - foreach ($userPermissions as $userPermission) - { - $permissionsCache[] = [ - 'user_id' => $userPermission->user_id, - 'album_id' => $album->id, - 'permission_id' => $userPermission->permission_id, - 'created_at' => new \DateTime(), - 'updated_at' => new \DateTime() - ]; - } - - $groupPermissions = array_filter($albumGroupPermissions->toArray(), function($item) use ($effectiveAlbumID) - { - return ($item->album_id == $effectiveAlbumID); - }); - - foreach ($groupPermissions as $groupPermission) - { - // Get a list of users in this group, and add one per user - $usersInGroup = array_filter($userGroups->toArray(), function($item) use ($groupPermission) - { - return $item->group_id = $groupPermission->group_id; - }); - - foreach ($usersInGroup as $userGroup) + foreach ($defaultAnonPermissions as $anonymousPermission) { $permissionsCache[] = [ - 'user_id' => $userGroup->user_id, 'album_id' => $album->id, - 'permission_id' => $groupPermission->permission_id, + 'permission_id' => $anonymousPermission->permission_id, 'created_at' => new \DateTime(), 'updated_at' => new \DateTime() ]; } + + foreach ($defaultAlbumUserPermissions as $userPermission) + { + $permissionsCache[] = [ + 'user_id' => $userPermission->user_id, + 'album_id' => $album->id, + 'permission_id' => $userPermission->permission_id, + 'created_at' => new \DateTime(), + 'updated_at' => new \DateTime() + ]; + } + + foreach ($defaultAlbumGroupPermissions as $groupPermission) + { + // Get a list of users in this group, and add one per user + $usersInGroup = array_filter($userGroups->toArray(), function ($item) use ($groupPermission) + { + return $item->group_id = $groupPermission->group_id; + }); + + foreach ($usersInGroup as $userGroup) + { + $permissionsCache[] = [ + 'user_id' => $userGroup->user_id, + 'album_id' => $album->id, + 'permission_id' => $groupPermission->permission_id, + 'created_at' => new \DateTime(), + 'updated_at' => new \DateTime() + ]; + } + } + } + else + { + /* Use the specified album-specific permissions */ + $anonymousPermissions = array_filter($albumAnonPermissions->toArray(), function ($item) use ($effectiveAlbumID) + { + return ($item->album_id == $effectiveAlbumID); + }); + + foreach ($anonymousPermissions as $anonymousPermission) + { + $permissionsCache[] = [ + 'album_id' => $album->id, + 'permission_id' => $anonymousPermission->permission_id, + 'created_at' => new \DateTime(), + 'updated_at' => new \DateTime() + ]; + } + + $userPermissions = array_filter($albumUserPermissions->toArray(), function ($item) use ($effectiveAlbumID) + { + return ($item->album_id == $effectiveAlbumID); + }); + + foreach ($userPermissions as $userPermission) + { + $permissionsCache[] = [ + 'user_id' => $userPermission->user_id, + 'album_id' => $album->id, + 'permission_id' => $userPermission->permission_id, + 'created_at' => new \DateTime(), + 'updated_at' => new \DateTime() + ]; + } + + $groupPermissions = array_filter($albumGroupPermissions->toArray(), function ($item) use ($effectiveAlbumID) + { + return ($item->album_id == $effectiveAlbumID); + }); + + foreach ($groupPermissions as $groupPermission) + { + // Get a list of users in this group, and add one per user + $usersInGroup = array_filter($userGroups->toArray(), function ($item) use ($groupPermission) + { + return $item->group_id = $groupPermission->group_id; + }); + + foreach ($usersInGroup as $userGroup) + { + $permissionsCache[] = [ + 'user_id' => $userGroup->user_id, + 'album_id' => $album->id, + 'permission_id' => $groupPermission->permission_id, + 'created_at' => new \DateTime(), + 'updated_at' => new \DateTime() + ]; + } + } } } diff --git a/app/Http/Controllers/Admin/AlbumController.php b/app/Http/Controllers/Admin/AlbumController.php index 21791d7..f8582a0 100644 --- a/app/Http/Controllers/Admin/AlbumController.php +++ b/app/Http/Controllers/Admin/AlbumController.php @@ -3,6 +3,9 @@ namespace App\Http\Controllers\Admin; use App\Album; +use App\AlbumDefaultAnonymousPermission; +use App\AlbumDefaultGroupPermission; +use App\AlbumDefaultUserPermission; use App\AlbumRedirect; use App\Facade\Theme; use App\Facade\UserConfig; @@ -28,6 +31,30 @@ use Illuminate\Support\Facades\View; class AlbumController extends Controller { + public static function doesGroupHaveDefaultPermission(Group $group, Permission $permission) + { + return AlbumDefaultGroupPermission::where([ + 'group_id' => $group->id, + 'permission_id' => $permission->id + ])->count() > 0; + } + + public static function doesUserHaveDefaultPermission($user, Permission $permission) + { + // User will be null for anonymous users + if (is_null($user)) + { + return AlbumDefaultAnonymousPermission::where(['permission_id' => $permission->id])->count() > 0; + } + else + { + return AlbumDefaultUserPermission::where([ + 'user_id' => $user->id, + 'permission_id' => $permission->id + ])->count() > 0; + } + } + public function __construct() { $this->middleware('auth'); @@ -83,6 +110,41 @@ class AlbumController extends Controller ]); } + public function defaultPermissions() + { + $this->authorizeAccessToAdminPanel('admin:manage-albums'); + + $addNewGroups = []; + $existingGroups = []; + foreach (Group::orderBy('name')->get() as $group) + { + if (AlbumDefaultGroupPermission::where('group_id', $group->id)->count() == 0) + { + $addNewGroups[] = $group; + } + else + { + $existingGroups[] = $group; + } + } + + $existingUsers = []; + foreach (User::orderBy('name')->get() as $user) + { + if (AlbumDefaultUserPermission::where('user_id', $user->id)->count() > 0) + { + $existingUsers[] = $user; + } + } + + return Theme::render('admin.album_default_permissions', [ + 'add_new_groups' => $addNewGroups, + 'all_permissions' => Permission::where('section', 'album')->get(), + 'existing_groups' => $existingGroups, + 'existing_users' => $existingUsers + ]); + } + public function delete($id) { $this->authorizeAccessToAdminPanel('admin:manage-albums'); @@ -215,6 +277,130 @@ class AlbumController extends Controller ]); } + public function setDefaultGroupPermissions(Request $request) + { + $this->authorizeAccessToAdminPanel('admin:manage-albums'); + + if ($request->get('action') == 'add_group' && $request->has('group_id')) + { + /* Add a new group to the default permission list */ + + /** @var Group $group */ + $group = Group::where('id', $request->get('group_id'))->first(); + if (is_null($group)) + { + App::abort(404); + } + + // Link all default permissions to the group + /** @var Permission $permission */ + foreach (Permission::where(['section' => 'album', 'is_default' => true])->get() as $permission) + { + $defaultPermission = new AlbumDefaultGroupPermission(); + $defaultPermission->group_id = $group->id; + $defaultPermission->permission_id = $permission->id; + $defaultPermission->save(); + } + } + else if ($request->get('action') == 'update_group_permissions') + { + /* Update existing group permissions for this album */ + AlbumDefaultGroupPermission::truncate(); + + $permissions = $request->get('permissions'); + if (is_array($permissions)) + { + foreach ($permissions as $groupID => $permissionIDs) + { + foreach ($permissionIDs as $permissionID) + { + $defaultPermission = new AlbumDefaultGroupPermission(); + $defaultPermission->group_id = $groupID; + $defaultPermission->permission_id = $permissionID; + $defaultPermission->save(); + } + } + } + } + + // Rebuild the permissions cache + $helper = new PermissionsHelper(); + $helper->rebuildCache(); + + return redirect(route('albums.defaultPermissions')); + } + + public function setDefaultUserPermissions(Request $request) + { + $this->authorizeAccessToAdminPanel('admin:manage-albums'); + + if ($request->get('action') == 'add_user' && $request->has('user_id')) + { + /* Add a new user to the permission list for this album */ + + /** @var User $user */ + $user = User::where('id', $request->get('user_id'))->first(); + if (is_null($user)) + { + App::abort(404); + } + + // Link all default permissions to the group + /** @var Permission $permission */ + foreach (Permission::where(['section' => 'album', 'is_default' => true])->get() as $permission) + { + $defaultPermission = new AlbumDefaultUserPermission(); + $defaultPermission->user_id = $user->id; + $defaultPermission->permission_id = $permission->id; + $defaultPermission->save(); + } + } + else if ($request->get('action') == 'update_user_permissions') + { + /* Update existing user and anonymous permissions for this album */ + AlbumDefaultAnonymousPermission::truncate(); + AlbumDefaultUserPermission::truncate(); + + $permissions = $request->get('permissions'); + if (is_array($permissions)) + { + if (isset($permissions['anonymous'])) + { + foreach ($permissions['anonymous'] as $permissionID) + { + $defaultPermission = new AlbumDefaultAnonymousPermission(); + $defaultPermission->permission_id = $permissionID; + $defaultPermission->save(); + } + } + + foreach ($permissions as $key => $value) + { + $userID = intval($key); + if ($userID == 0) + { + // Skip non-numeric IDs (e.g. anonymous) + continue; + } + + foreach ($value as $permissionID) + { + $defaultPermission = new AlbumDefaultUserPermission(); + $defaultPermission->user_id = $userID; + $defaultPermission->permission_id = $permissionID; + $defaultPermission->save(); + } + } + } + } + + // Rebuild the permissions cache + $helper = new PermissionsHelper(); + $helper->rebuildCache(); + + return redirect(route('albums.defaultPermissions')); + } + public function setGroupPermissions(Request $request, $id) { $this->authorizeAccessToAdminPanel('admin:manage-albums'); @@ -474,21 +660,41 @@ class AlbumController extends Controller $album->generateUrlPath(); $album->save(); - // Link all default permissions to anonymous users (if a public album) - if (!$album->is_permissions_inherited) + // Link the default permissions (if a public album) + $isPrivate = (strtolower($request->get('is_private')) == 'on'); + if (!$album->is_permissions_inherited && !$isPrivate) { - $isPrivate = (strtolower($request->get('is_private')) == 'on'); + $defaultAlbumUserPermissions = AlbumDefaultUserPermission::all(); + $defaultAlbumGroupPermissions = AlbumDefaultGroupPermission::all(); + $defaultAnonPermissions = AlbumDefaultAnonymousPermission::all(); - if (!$isPrivate) + /** @var AlbumDefaultAnonymousPermission $permission */ + foreach ($defaultAnonPermissions as $permission) { - /** @var Permission $permission */ - foreach (Permission::where(['section' => 'album', 'is_default' => true])->get() as $permission) - { - $album->anonymousPermissions()->attach($permission->id, [ - 'created_at' => new \DateTime(), - 'updated_at' => new \DateTime() - ]); - } + $album->anonymousPermissions()->attach($permission->permission_id, [ + 'created_at' => new \DateTime(), + 'updated_at' => new \DateTime() + ]); + } + + /** @var AlbumDefaultGroupPermission $permission */ + foreach ($defaultAlbumGroupPermissions as $permission) + { + $album->groupPermissions()->attach($permission->permission_id, [ + 'group_id' => $permission->group_id, + 'created_at' => new \DateTime(), + 'updated_at' => new \DateTime() + ]); + } + + /** @var AlbumDefaultUserPermission $permission */ + foreach ($defaultAlbumUserPermissions as $permission) + { + $album->userPermissions()->attach($permission->permission_id, [ + 'user_id' => $permission->user_id, + 'created_at' => new \DateTime(), + 'updated_at' => new \DateTime() + ]); } } diff --git a/app/Http/Controllers/Admin/DefaultController.php b/app/Http/Controllers/Admin/DefaultController.php index 8be8d16..60bf4b6 100644 --- a/app/Http/Controllers/Admin/DefaultController.php +++ b/app/Http/Controllers/Admin/DefaultController.php @@ -16,6 +16,7 @@ use App\Http\Requests\SaveSettingsRequest; use App\Label; use App\Mail\TestMailConfig; use App\Photo; +use App\PhotoComment; use App\Services\GiteaService; use App\Services\GithubService; use App\Services\PhotoService; @@ -140,6 +141,7 @@ class DefaultController extends Controller $photoCount = Photo::all()->count(); $groupCount = Group::all()->count(); $labelCount = Label::all()->count(); + $commentCount = PhotoComment::whereNotNull('approved_at')->count(); $userCount = User::where('is_activated', true)->count(); $minMetadataVersion = Photo::min('metadata_version'); @@ -157,6 +159,7 @@ class DefaultController extends Controller return Theme::render('admin.index', [ 'album_count' => $albumCount, 'app_version' => config('app.version'), + 'comment_count' => $commentCount, 'group_count' => $groupCount, 'label_count' => $labelCount, 'memory_limit' => ini_get('memory_limit'), @@ -231,9 +234,13 @@ class DefaultController extends Controller $checkboxKeys = [ 'albums_menu_parents_only', + 'allow_photo_comments', + 'allow_photo_comments_anonymous', 'allow_self_registration', 'enable_visitor_hits', 'hotlink_protection', + 'moderate_anonymous_users', + 'moderate_known_users', 'recaptcha_enabled_registration', 'remove_copyright', 'require_email_verification', @@ -252,6 +259,8 @@ class DefaultController extends Controller 'facebook_app_secret', 'google_app_id', 'google_app_secret', + 'photo_comments_allowed_html', + 'photo_comments_thread_depth', 'sender_address', 'sender_name', 'smtp_server', diff --git a/app/Http/Controllers/Admin/GroupController.php b/app/Http/Controllers/Admin/GroupController.php index 70a237f..40b7d8f 100644 --- a/app/Http/Controllers/Admin/GroupController.php +++ b/app/Http/Controllers/Admin/GroupController.php @@ -34,7 +34,7 @@ class GroupController extends Controller public function delete($id) { - $this->authorizeAccessToAdminPanel(); + $this->authorizeAccessToAdminPanel('admin:manage-groups'); $group = Group::where('id', intval($id))->first(); if (is_null($group)) diff --git a/app/Http/Controllers/Admin/PhotoCommentController.php b/app/Http/Controllers/Admin/PhotoCommentController.php new file mode 100644 index 0000000..ce41bed --- /dev/null +++ b/app/Http/Controllers/Admin/PhotoCommentController.php @@ -0,0 +1,375 @@ +middleware('auth'); + View::share('is_admin', true); + } + + public function applyBulkAction(Request $request) + { + $this->authorizeAccessToAdminPanel('admin:manage-comments'); + + $commentIDs = $request->get('comment_ids'); + if (is_null($commentIDs) || !is_array($commentIDs) || count($commentIDs) == 0) + { + $request->session()->flash('warning', trans('admin.no_comments_selected_message')); + return redirect(route('comments.index')); + } + + $comments = PhotoComment::whereIn('id', $commentIDs)->get(); + $commentsActioned = 0; + + if ($request->has('bulk_delete')) + { + /** @var PhotoComment $comment */ + foreach ($comments as $comment) + { + $comment->delete(); + $commentsActioned++; + } + + $request->session()->flash('success', trans_choice('admin.bulk_comments_deleted', $commentsActioned, ['number' => $commentsActioned])); + } + else if ($request->has('bulk_approve')) + { + /** @var PhotoComment $comment */ + foreach ($comments as $comment) + { + if ($comment->isApproved()) + { + // Don't make changes if already approved + continue; + } + + // Mark as approved + $comment->approved_at = new \DateTime(); + $comment->approved_user_id = $this->getUser()->id; + + // The comment may have already been rejected - remove the data if so + $comment->rejected_at = null; + $comment->rejected_user_id = null; + + // Send the notification e-mail to the owner + + $comment->save(); + $commentsActioned++; + + // Send e-mail notification + $photo = $comment->photo; + $album = $photo->album; + $this->notifyAlbumOwnerAndPoster($album, $photo, $comment); + } + + $request->session()->flash('success', trans_choice('admin.bulk_comments_approved', $commentsActioned, ['number' => $commentsActioned])); + } + else if ($request->has('bulk_reject')) + { + /** @var PhotoComment $comment */ + foreach ($comments as $comment) + { + if ($comment->isRejected()) + { + // Don't make changes if already rejected + continue; + } + + // Mark as rejected + $comment->rejected_at = new \DateTime(); + $comment->rejected_user_id = $this->getUser()->id; + + // The comment may have already been approved - remove the data if so + $comment->approved_at = null; + $comment->approved_user_id = null; + + $comment->save(); + $commentsActioned++; + } + + $request->session()->flash('success', trans_choice('admin.bulk_comments_approved', $commentsActioned, ['number' => $commentsActioned])); + } + + return redirect(route('comments.index')); + } + + public function approve($id) + { + $this->authorizeAccessToAdminPanel('admin:manage-comments'); + + $comment = $this->loadCommentByID($id); + + return Theme::render('admin.approve_comment', ['comment' => $comment]); + } + + public function bulkAction(Request $request) + { + $this->authorizeAccessToAdminPanel('admin:manage-comments'); + + $commentIDs = $request->get('comment_ids'); + if (is_null($commentIDs) || !is_array($commentIDs) || count($commentIDs) == 0) + { + $request->session()->flash('warning', trans('admin.no_comments_selected_message')); + return redirect(route('comments.index')); + } + + if ($request->has('bulk_delete')) + { + if (count($commentIDs) == 1) + { + // Single comment selected - redirect to the single delete page + return redirect(route('comments.delete', ['id' => $commentIDs[0]])); + } + + // Show the view to confirm the delete + return Theme::render('admin.bulk_delete_comments', [ + 'comment_count' => count($commentIDs), + 'comment_ids' => $commentIDs + ]); + } + else if ($request->has('bulk_approve')) + { + if (count($commentIDs) == 1) + { + // Single comment selected - redirect to the single approve page + return redirect(route('comments.approve', ['id' => $commentIDs[0]])); + } + + // Show the view to confirm the approval + return Theme::render('admin.bulk_approve_comments', [ + 'comment_count' => count($commentIDs), + 'comment_ids' => $commentIDs + ]); + } + else if ($request->has('bulk_reject')) + { + if (count($commentIDs) == 1) + { + // Single comment selected - redirect to the single reject page + return redirect(route('comments.reject', ['id' => $commentIDs[0]])); + } + + // Show the view to confirm the rejection + return Theme::render('admin.bulk_reject_comments', [ + 'comment_count' => count($commentIDs), + 'comment_ids' => $commentIDs + ]); + } + + // Unrecognised action - simply redirect back to the index page + return redirect(route('comments.index')); + } + + public function confirmApprove(Request $request, $id) + { + $this->authorizeAccessToAdminPanel('admin:manage-comments'); + + $comment = $this->loadCommentByID($id); + + if ($comment->isApproved()) + { + // Comment has already been approved + return redirect(route('comments.index')); + } + + // Mark as approved + $comment->approved_at = new \DateTime(); + $comment->approved_user_id = $this->getUser()->id; + + // The comment may have already been rejected - remove the data if so + $comment->rejected_at = null; + $comment->rejected_user_id = null; + + $comment->save(); + + $request->session()->flash('success', trans('admin.comment_approval_successful', [ + 'author_name' => $comment->authorDisplayName() + ])); + + // Send e-mail notification + $photo = $comment->photo; + $album = $photo->album; + $this->notifyAlbumOwnerAndPoster($album, $photo, $comment); + + return redirect(route('comments.index')); + } + + public function confirmReject(Request $request, $id) + { + $this->authorizeAccessToAdminPanel('admin:manage-comments'); + + $comment = $this->loadCommentByID($id); + + if ($comment->isRejected()) + { + // Comment has already been rejected + return redirect(route('comments.index')); + } + + // Mark as rejected + $comment->rejected_at = new \DateTime(); + $comment->rejected_user_id = $this->getUser()->id; + + // The comment may have already been approved - remove the data if so + $comment->approved_at = null; + $comment->approved_user_id = null; + + $comment->save(); + + $request->session()->flash('success', trans('admin.comment_rejection_successful', [ + 'author_name' => $comment->authorDisplayName() + ])); + + return redirect(route('comments.index')); + } + + public function delete($id) + { + $this->authorizeAccessToAdminPanel('admin:manage-comments'); + + $comment = $this->loadCommentByID($id); + + return Theme::render('admin.delete_comment', ['comment' => $comment]); + } + + public function destroy(Request $request, $id) + { + $this->authorizeAccessToAdminPanel('admin:manage-comments'); + + /** @var PhotoComment $comment */ + $comment = $this->loadCommentByID($id); + + $comment->delete(); + $request->session()->flash('success', trans('admin.comment_deletion_successful', [ + 'author_name' => $comment->authorDisplayName() + ])); + + return redirect(route('comments.index')); + } + + public function index(Request $request) + { + $this->authorizeAccessToAdminPanel('admin:manage-comments'); + + $validStatusList = [ + 'all', + 'pending', + 'approved', + 'rejected' + ]; + + $filterStatus = $request->get('status', 'all'); + if (!in_array($filterStatus, $validStatusList)) + { + $filterStatus = $validStatusList[0]; + } + + $comments = PhotoComment::with('photo') + ->with('photo.album') + ->orderBy('created_at', 'desc'); + + switch (strtolower($filterStatus)) + { + case 'approved': + $comments->whereNotNull('approved_at') + ->whereNull('rejected_at'); + break; + + case 'pending': + $comments->whereNull('approved_at') + ->whereNull('rejected_at'); + break; + + case 'rejected': + $comments->whereNull('approved_at') + ->whereNotNull('rejected_at'); + break; + } + + return Theme::render('admin.list_comments', [ + 'comments' => $comments->paginate(UserConfig::get('items_per_page')), + 'filter_status' => $filterStatus, + 'success' => $request->session()->get('success'), + 'warning' => $request->session()->get('warning') + ]); + } + + public function reject($id) + { + $this->authorizeAccessToAdminPanel('admin:manage-comments'); + + $comment = $this->loadCommentByID($id); + + return Theme::render('admin.reject_comment', ['comment' => $comment]); + } + + /** + * Loads a given comment by its ID. + * @param $id + * @return PhotoComment + */ + private function loadCommentByID($id) + { + $comment = PhotoComment::where('id', intval($id))->first(); + if (is_null($comment)) + { + App::abort(404); + } + + return $comment; + } + + /** + * Sends an e-mail notification to an album's owned that a comment has been posted/approved. + * @param Album $album + * @param Photo $photo + * @param PhotoComment $comment + */ + private function notifyAlbumOwnerAndPoster(Album $album, Photo $photo, PhotoComment $comment) + { + $owner = $album->user; + + Mail::to($owner)->send(new PhotoCommentApproved($owner, $album, $photo, $comment)); + + // Also send a notification to the comment poster + $poster = new User(); + $poster->name = $comment->authorDisplayName(); + $poster->email = $comment->authorEmail(); + + Mail::to($poster)->send(new PhotoCommentApprovedUser($poster, $album, $photo, $comment)); + + // Send notification to the parent comment owner (if this is a reply) + if (!is_null($comment->parent_comment_id)) + { + $parentComment = $this->loadCommentByID($comment->parent_comment_id); + + if (is_null($parentComment)) + { + return; + } + + $parentPoster = new User(); + $parentPoster->name = $parentComment->authorDisplayName(); + $parentPoster->email = $parentComment->authorEmail(); + + Mail::to($parentPoster)->send(new PhotoCommentRepliedTo($parentPoster, $album, $photo, $comment)); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 53009e4..257bee4 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -6,6 +6,7 @@ use App\Facade\Theme; use App\Facade\UserConfig; use App\Http\Controllers\Controller; use App\User; +use Illuminate\Contracts\Routing\UrlGenerator; use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Http\Request; use Laravel\Socialite\One\TwitterProvider; @@ -29,6 +30,8 @@ class LoginController extends Controller use AuthenticatesUsers; + protected $generator; + /** * Where to redirect users after login / registration. * @@ -41,9 +44,19 @@ class LoginController extends Controller * * @return void */ - public function __construct() + public function __construct(UrlGenerator $generator) { $this->middleware('guest', ['except' => 'logout']); + $this->generator = $generator; + } + + public function logout(Request $request) + { + $this->guard()->logout(); + + $request->session()->invalidate(); + + return redirect()->back(); } protected function attemptLogin(Request $request) @@ -88,6 +101,8 @@ class LoginController extends Controller */ public function showLoginForm(Request $request) { + $request->getSession()->put('url.intended', $this->generator->previous(false)); + return Theme::render('auth.v2_unified', [ 'active_tab' => 'login', 'info' => $request->session()->get('info'), diff --git a/app/Http/Controllers/Gallery/PhotoCommentController.php b/app/Http/Controllers/Gallery/PhotoCommentController.php new file mode 100644 index 0000000..3ae5db5 --- /dev/null +++ b/app/Http/Controllers/Gallery/PhotoCommentController.php @@ -0,0 +1,371 @@ +loadAlbumPhotoComment($albumUrlAlias, $photoFilename, $commentID, $album, $photo, $comment)) + { + return null; + } + + if (!User::currentOrAnonymous()->can('moderate-comments', $photo)) + { + App::abort(403); + return null; + } + + if (!$comment->isModerated()) + { + if ($request->has('approve')) + { + $comment->approved_at = new \DateTime(); + $comment->approved_user_id = $this->getUser()->id; + $comment->save(); + + $this->notifyAlbumOwnerAndPoster($album, $photo, $comment); + $request->getSession()->flash('success', trans('gallery.photo_comment_approved_successfully')); + } + else if ($request->has('reject')) + { + $comment->rejected_at = new \DateTime(); + $comment->rejected_user_id = $this->getUser()->id; + $comment->save(); + + $request->getSession()->flash('success', trans('gallery.photo_comment_rejected_successfully')); + } + } + + return redirect($photo->url()); + } + + public function reply(Request $request, $albumUrlAlias, $photoFilename, $commentID) + { + $album = null; + + /** @var Photo $photo */ + $photo = null; + + /** @var PhotoComment $comment */ + $comment = null; + + if (!$this->loadAlbumPhotoComment($albumUrlAlias, $photoFilename, $commentID, $album, $photo, $comment)) + { + return null; + } + + if (!User::currentOrAnonymous()->can('post-comment', $photo)) + { + App::abort(403); + return null; + } + + return Theme::render('partials.photo_comments_reply_form', [ + 'photo' => $photo, + 'reply_comment' => $comment + ]); + } + + public function store(Request $request, $albumUrlAlias, $photoFilename) + { + $album = null; + + /** @var Photo $photo */ + $photo = null; + + /** @var PhotoComment $comment */ + $comment = null; + + if (!$this->loadAlbumPhotoComment($albumUrlAlias, $photoFilename, 0, $album, $photo, $comment)) + { + return null; + } + + if (!User::currentOrAnonymous()->can('post-comment', $photo)) + { + App::abort(403); + return null; + } + + // Validate and link the parent comment, if provided + // We do this here so if the validation fails, we still have the parent comment available in the catch block + $parentComment = null; + if ($request->has('parent_comment_id')) + { + $parentComment = $photo->comments()->where('id', intval($request->get('parent_comment_id')))->first(); + + if (is_null($parentComment)) + { + return redirect($photo->url()); + } + } + + try + { + $this->validate($request, [ + 'name' => 'required|max:255', + 'email' => 'sometimes|max:255|email', + 'comment' => 'required' + ]); + + $commentText = $this->stripDisallowedHtmlTags($request->get('comment')); + + $comment = new PhotoComment(); + $comment->photo_id = $photo->id; + $comment->fill($request->only(['name', 'email'])); + $comment->comment = $commentText; + + if (!is_null($parentComment)) + { + $comment->parent_comment_id = $parentComment->id; + } + + // Set the created user ID if we're logged in + $user = $this->getUser(); + if (!is_null($user) && !$user->isAnonymous()) + { + $comment->created_user_id = $user->id; + } + + // Auto-approve the comment if we're allowed to moderate comments + $isAutoApproved = false; + if (User::currentOrAnonymous()->can('moderate-comments', $photo)) + { + $comment->approved_at = new \DateTime(); + $comment->approved_user_id = $user->id; + $isAutoApproved = true; + } + + // Auto-approve the comment if settings allow + if ($user->isAnonymous() && !UserConfig::get('moderate_anonymous_users')) + { + $comment->approved_at = new \DateTime(); + $comment->approved_user_id = null; // we don't have a user ID to set! + $isAutoApproved = true; + } + else if (!$user->isAnonymous() && !UserConfig::get('moderate_known_users')) + { + $comment->approved_at = new \DateTime(); + $comment->approved_user_id = $user->id; + $isAutoApproved = true; + } + + $comment->save(); + + // Send notification e-mails to moderators or album owner + if (!$isAutoApproved) + { + $this->notifyAlbumModerators($album, $photo, $comment); + $request->getSession()->flash('success', trans('gallery.photo_comment_posted_successfully_pending_moderation')); + } + else + { + $this->notifyAlbumOwnerAndPoster($album, $photo, $comment); + $request->getSession()->flash('success', trans('gallery.photo_comment_posted_successfully')); + } + + if ($request->isXmlHttpRequest()) + { + return response()->json(['redirect_url' => $photo->url()]); + } + else + { + return redirect($photo->url()); + } + } + catch (ValidationException $e) + { + if (!is_null($parentComment)) + { + return redirect() + ->to($photo->replyToCommentFormUrl($parentComment->id)) + ->withErrors($e->errors()) + ->withInput($request->all()); + } + else + { + return redirect() + ->back() + ->withErrors($e->errors()) + ->withInput($request->all()); + } + } + } + + private function loadAlbumPhotoComment($albumUrlAlias, $photoFilename, $commentID, &$album, &$photo, &$comment) + { + $album = DbHelper::getAlbumByPath($albumUrlAlias); + if (is_null($album)) + { + App::abort(404); + return false; + } + + $this->authorizeForUser($this->getUser(), 'view', $album); + + $photo = PhotoController::loadPhotoByAlbumAndFilename($album, $photoFilename); + + if (!UserConfig::get('allow_photo_comments')) + { + // Not allowed to post comments + App::abort(404); + return false; + } + + if (intval($commentID > 0)) + { + $comment = $photo->comments()->where('id', $commentID)->first(); + if (is_null($comment)) + { + App::abort(404); + return false; + } + } + + return true; + } + + /** + * Sends an e-mail notification to an album's moderators that a comment is available to moderate. + * @param Album $album + * @param Photo $photo + * @param PhotoComment $comment + */ + private function notifyAlbumModerators(Album $album, Photo $photo, PhotoComment $comment) + { + // Get all users from the cache + $helper = new PermissionsHelper(); + $moderators = $helper->usersWhoCan_Album($album, 'moderate-comments'); + + /** @var User $moderator */ + foreach ($moderators as $moderator) + { + Mail::to($moderator)->send(new ModeratePhotoComment($moderator, $album, $photo, $comment)); + } + } + + /** + * Sends an e-mail notification to an album's owned that a comment has been posted/approved. + * @param Album $album + * @param Photo $photo + * @param PhotoComment $comment + */ + private function notifyAlbumOwnerAndPoster(Album $album, Photo $photo, PhotoComment $comment) + { + $owner = $album->user; + + Mail::to($owner)->send(new PhotoCommentApproved($owner, $album, $photo, $comment)); + + // Also send a notification to the comment poster + $poster = new User(); + $poster->name = $comment->authorDisplayName(); + $poster->email = $comment->authorEmail(); + + Mail::to($poster)->send(new PhotoCommentApprovedUser($poster, $album, $photo, $comment)); + + // Send notification to the parent comment owner (if this is a reply) + if (!is_null($comment->parent_comment_id)) + { + $parentComment = $this->loadCommentByID($comment->parent_comment_id); + + if (is_null($parentComment)) + { + return; + } + + $parentPoster = new User(); + $parentPoster->name = $parentComment->authorDisplayName(); + $parentPoster->email = $parentComment->authorEmail(); + + Mail::to($parentPoster)->send(new PhotoCommentRepliedTo($parentPoster, $album, $photo, $comment)); + } + } + + private function stripDisallowedHtmlTags($commentText) + { + $allowedHtmlTags = explode(',', UserConfig::get('photo_comments_allowed_html')); + $allowedHtmlTagsCleaned = []; + + foreach ($allowedHtmlTags as $tag) + { + $allowedHtmlTagsCleaned[] = trim($tag); + } + + // Match any starting HTML tags + $regexMatchString = '/<(?!\/)([a-z]+)(?:\s.*)*>/Us'; + + $htmlTagMatches = []; + preg_match_all($regexMatchString, $commentText, $htmlTagMatches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER); + + for ($index = 0; $index < count($htmlTagMatches); $index++) + { + $htmlTagMatch = $htmlTagMatches[$index]; + + $htmlTag = $htmlTagMatch[1][0]; // e.g. "p" for

+ if (in_array($htmlTag, $allowedHtmlTagsCleaned)) + { + // This tag is allowed - carry on + continue; + } + + /* This tag is not allowed - remove it from the string */ + + // Find the closing tag + $disallowedStringOffset = $htmlTagMatch[0][1]; + $endingTagMatches = []; + preg_match(sprintf('/(<%1$s.*>)(.+)<\/%1$s>/Us', $htmlTag), $commentText, $endingTagMatches, 0, $disallowedStringOffset); + + // Replace the matched string with the inner string + $commentText = substr_replace($commentText, $endingTagMatches[2], $disallowedStringOffset, strlen($endingTagMatches[0])); + + // Adjust the offsets for strings after the one we're processing, so the offsets match up with the string correctly + for ($index2 = $index + 1; $index2 < count($htmlTagMatches); $index2++) + { + // If this string appears entirely BEFORE the next one starts, we need to subtract the entire length. + // Otherwise, we only need to substract the length of the start tag, as the next one starts within it. + $differenceAfterReplacement = strlen($endingTagMatches[1]); + + if ($htmlTagMatch[0][1] + strlen($endingTagMatches[0]) < $htmlTagMatches[$index2][0][1]) + { + $differenceAfterReplacement = strlen($endingTagMatches[0]) - strlen($endingTagMatches[2]); + } + + $htmlTagMatches[$index2][0][1] -= $differenceAfterReplacement; + $htmlTagMatches[$index2][1][1] -= $differenceAfterReplacement; + } + } + + return $commentText; + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Gallery/PhotoController.php b/app/Http/Controllers/Gallery/PhotoController.php index bd50bb0..98a3eb8 100644 --- a/app/Http/Controllers/Gallery/PhotoController.php +++ b/app/Http/Controllers/Gallery/PhotoController.php @@ -157,7 +157,8 @@ class PhotoController extends Controller 'is_original_allowed' => $isOriginalAllowed, 'next_photo' => $nextPhoto, 'photo' => $photo, - 'previous_photo' => $previousPhoto + 'previous_photo' => $previousPhoto, + 'success' => $request->getSession()->get('success') ]); } diff --git a/app/Mail/ModeratePhotoComment.php b/app/Mail/ModeratePhotoComment.php new file mode 100644 index 0000000..db6db37 --- /dev/null +++ b/app/Mail/ModeratePhotoComment.php @@ -0,0 +1,56 @@ +user = $user; + $this->album = $album; + $this->photo = $photo; + $this->comment = $comment; + } + + /** + * Build the message. + * + * @return $this + */ + public function build() + { + $subject = trans('email.moderate_photo_comment_subject', ['album_name' => $this->album->name]); + + return $this + ->subject($subject) + ->markdown(Theme::viewName('email.moderate_photo_comment')) + ->with([ + 'album' => $this->album, + 'comment' => $this->comment, + 'photo' => $this->photo, + 'subject' => $subject, + 'user' => $this->user + ]); + } +} diff --git a/app/Mail/PhotoCommentApproved.php b/app/Mail/PhotoCommentApproved.php new file mode 100644 index 0000000..fb3a326 --- /dev/null +++ b/app/Mail/PhotoCommentApproved.php @@ -0,0 +1,60 @@ +user = $user; + $this->album = $album; + $this->photo = $photo; + $this->comment = $comment; + } + + /** + * Build the message. + * + * @return $this + */ + public function build() + { + $subject = trans('email.photo_comment_approved_subject', ['album_name' => $this->album->name]); + + return $this + ->subject($subject) + ->markdown(Theme::viewName('email.photo_comment_approved')) + ->with([ + 'album' => $this->album, + 'comment' => $this->comment, + 'photo' => $this->photo, + 'subject' => $subject, + 'user' => $this->user + ]); + } +} diff --git a/app/Mail/PhotoCommentApprovedUser.php b/app/Mail/PhotoCommentApprovedUser.php new file mode 100644 index 0000000..c791a6f --- /dev/null +++ b/app/Mail/PhotoCommentApprovedUser.php @@ -0,0 +1,60 @@ +user = $user; + $this->album = $album; + $this->photo = $photo; + $this->comment = $comment; + } + + /** + * Build the message. + * + * @return $this + */ + public function build() + { + $subject = trans('email.photo_comment_approved_user_subject', ['album_name' => $this->album->name]); + + return $this + ->subject($subject) + ->markdown(Theme::viewName('email.photo_comment_approved_user')) + ->with([ + 'album' => $this->album, + 'comment' => $this->comment, + 'photo' => $this->photo, + 'subject' => $subject, + 'user' => $this->user + ]); + } +} diff --git a/app/Mail/PhotoCommentRepliedTo.php b/app/Mail/PhotoCommentRepliedTo.php new file mode 100644 index 0000000..77dcebc --- /dev/null +++ b/app/Mail/PhotoCommentRepliedTo.php @@ -0,0 +1,60 @@ +user = $user; + $this->album = $album; + $this->photo = $photo; + $this->comment = $comment; + } + + /** + * Build the message. + * + * @return $this + */ + public function build() + { + $subject = trans('email.photo_comment_replied_to_subject', ['album_name' => $this->album->name]); + + return $this + ->subject($subject) + ->markdown(Theme::viewName('email.photo_comment_replied_to')) + ->with([ + 'album' => $this->album, + 'comment' => $this->comment, + 'photo' => $this->photo, + 'subject' => $subject, + 'user' => $this->user + ]); + } +} diff --git a/app/Photo.php b/app/Photo.php index 486e979..a4fef8b 100644 --- a/app/Photo.php +++ b/app/Photo.php @@ -52,6 +52,11 @@ class Photo extends Model return $this->belongsTo(Album::class); } + public function comments() + { + return $this->hasMany(PhotoComment::class); + } + public function exifUrl() { return route('viewExifData', [ @@ -77,6 +82,32 @@ class Photo extends Model return $this->belongsToMany(Label::class, 'photo_labels'); } + public function moderateCommentUrl($commentID = -1) + { + return route('moderatePhotoComment', [ + 'albumUrlAlias' => $this->album->url_path, + 'photoFilename' => $this->storage_file_name, + 'commentID' => $commentID + ]); + } + + public function postCommentUrl() + { + return route('postPhotoComment', [ + 'albumUrlAlias' => $this->album->url_path, + 'photoFilename' => $this->storage_file_name + ]); + } + + public function replyToCommentFormUrl($commentID = -1) + { + return route('replyPhotoComment', [ + 'albumUrlAlias' => $this->album->url_path, + 'photoFilename' => $this->storage_file_name, + 'commentID' => $commentID + ]); + } + public function thumbnailUrl($thumbnailName = null, $cacheBust = true) { $url = $this->album->getAlbumSource()->getUrlToPhoto($this, $thumbnailName); diff --git a/app/PhotoComment.php b/app/PhotoComment.php new file mode 100644 index 0000000..87f542f --- /dev/null +++ b/app/PhotoComment.php @@ -0,0 +1,91 @@ +createdBy) ? $this->name : $this->createdBy->name; + } + + public function authorEmail() + { + return is_null($this->createdBy) ? $this->email : $this->createdBy->email; + } + + public function children() + { + return $this->hasMany(PhotoComment::class, 'parent_comment_id'); + } + + public function createdBy() + { + return $this->belongsTo(User::class, 'created_user_id'); + } + + public function depth() + { + $depth = 0; + $current = $this; + + while (!is_null($current->parent)) + { + $current = $current->parent; + $depth++; + } + + return $depth; + } + + public function isApproved() + { + return (!is_null($this->approved_at) && is_null($this->rejected_at)); + } + + public function isModerated() + { + return (!is_null($this->approved_at) || !is_null($this->rejected_at)); + } + + public function isRejected() + { + return (!is_null($this->rejected_at) && is_null($this->approved_at)); + } + + public function parent() + { + return $this->belongsTo(PhotoComment::class, 'parent_comment_id'); + } + + public function photo() + { + return $this->belongsTo(Photo::class); + } + + public function textAsHtml() + { + $start = '

'; + $end = '

'; + $isHtml = ( + strlen($this->comment) > (strlen($start) + strlen($end)) && // text contains both our start + end string + strtolower(substr($this->comment, 0, strlen($start))) == strtolower($start) && // text starts with our start string + strtolower(substr($this->comment, strlen($this->comment) - strlen($end))) == strtolower($end) // text ends with our end string + ); + + return $isHtml ? $this->comment : sprintf('

%s

', $this->comment); + } +} \ No newline at end of file diff --git a/app/Policies/AlbumPolicy.php b/app/Policies/AlbumPolicy.php index 2309121..0eeed6f 100644 --- a/app/Policies/AlbumPolicy.php +++ b/app/Policies/AlbumPolicy.php @@ -3,6 +3,7 @@ namespace App\Policies; use App\Album; +use App\Facade\UserConfig; use App\Group; use App\Helpers\PermissionsHelper; use App\Permission; @@ -93,6 +94,34 @@ class AlbumPolicy return $this->userHasPermission($user, $album, 'manipulate-photos'); } + public function moderateComments(User $user, Album $album) + { + if ($user->id == $album->user_id) + { + // The album's owner and can do everything + return true; + } + + return $this->userHasPermission($user, $album, 'moderate-comments'); + } + + public function postComment(User $user, Album $album) + { + if ($user->id == $album->user_id) + { + // The album's owner and can do everything + return true; + } + + // Don't allow comments to be posted if anonymous user, and anonymous comments disabled + if ($user->isAnonymous() && !UserConfig::get('allow_photo_comments_anonymous')) + { + return false; + } + + return $this->userHasPermission($user, $album, 'post-comment'); + } + public function uploadPhotos(User $user, Album $album) { if ($user->id == $album->user_id) diff --git a/app/Policies/PhotoPolicy.php b/app/Policies/PhotoPolicy.php index 52c7119..eaa0626 100644 --- a/app/Policies/PhotoPolicy.php +++ b/app/Policies/PhotoPolicy.php @@ -61,4 +61,26 @@ class PhotoPolicy return $user->can('manipulate-photos', $photo->album); } + + public function moderateComments(User $user, Photo $photo) + { + if ($user->id == $photo->user_id) + { + // The photo's owner can do everything + return true; + } + + return $user->can('moderate-comments', $photo->album); + } + + public function postComment(User $user, Photo $photo) + { + if ($user->id == $photo->user_id) + { + // The photo's owner can do everything + return true; + } + + return $user->can('post-comment', $photo->album); + } } diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index d37e63a..556dfa2 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -54,6 +54,10 @@ class AuthServiceProvider extends ServiceProvider { return $this->userHasAdminPermission($user, 'manage-albums'); }); + Gate::define('admin:manage-comments', function ($user) + { + return $this->userHasAdminPermission($user, 'manage-comments'); + }); Gate::define('admin:manage-groups', function ($user) { return $this->userHasAdminPermission($user, 'manage-groups'); diff --git a/database/migrations/2018_09_17_132906_create_photo_comments_table.php b/database/migrations/2018_09_17_132906_create_photo_comments_table.php new file mode 100644 index 0000000..7c6f101 --- /dev/null +++ b/database/migrations/2018_09_17_132906_create_photo_comments_table.php @@ -0,0 +1,52 @@ +bigIncrements('id'); + $table->unsignedBigInteger('photo_id'); + $table->string('name'); + $table->string('email'); + $table->text('comment'); + $table->unsignedInteger('created_user_id')->nullable(true); + $table->unsignedInteger('approved_user_id')->nullable(true); + $table->dateTime('approved_at')->nullable(true); + $table->unsignedBigInteger('parent_comment_id')->nullable(true); + $table->timestamps(); + + $table->foreign('photo_id') + ->references('id')->on('photos') + ->onDelete('cascade'); + $table->foreign('created_user_id') + ->references('id')->on('users') + ->onDelete('cascade'); + $table->foreign('approved_user_id') + ->references('id')->on('users') + ->onDelete('cascade'); + $table->foreign('parent_comment_id') + ->references('id')->on('photo_comments') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('photo_comments'); + } +} diff --git a/database/migrations/2018_09_19_190631_add_comment_rejected_columns.php b/database/migrations/2018_09_19_190631_add_comment_rejected_columns.php new file mode 100644 index 0000000..788effb --- /dev/null +++ b/database/migrations/2018_09_19_190631_add_comment_rejected_columns.php @@ -0,0 +1,34 @@ +unsignedInteger('rejected_user_id')->nullable(true); + $table->dateTime('rejected_at')->nullable(true); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('photo_comments', function (Blueprint $table) { + $table->dropColumn('rejected_user_id'); + $table->dropColumn('rejected_at'); + }); + } +} diff --git a/database/migrations/2018_09_23_100536_create_album_default_group_permissions_table.php b/database/migrations/2018_09_23_100536_create_album_default_group_permissions_table.php new file mode 100644 index 0000000..d881618 --- /dev/null +++ b/database/migrations/2018_09_23_100536_create_album_default_group_permissions_table.php @@ -0,0 +1,40 @@ +unsignedInteger('group_id'); + $table->unsignedInteger('permission_id'); + + $table->foreign('group_id') + ->references('id')->on('groups') + ->onDelete('cascade'); + $table->foreign('permission_id') + ->references('id')->on('permissions') + ->onDelete('no action'); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('album_default_group_permissions'); + } +} diff --git a/database/migrations/2018_09_23_100542_create_album_default_user_permissions_table.php b/database/migrations/2018_09_23_100542_create_album_default_user_permissions_table.php new file mode 100644 index 0000000..2cf4b42 --- /dev/null +++ b/database/migrations/2018_09_23_100542_create_album_default_user_permissions_table.php @@ -0,0 +1,40 @@ +unsignedInteger('user_id'); + $table->unsignedInteger('permission_id'); + + $table->foreign('user_id') + ->references('id')->on('users') + ->onDelete('cascade'); + $table->foreign('permission_id') + ->references('id')->on('permissions') + ->onDelete('no action'); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('album_default_user_permissions'); + } +} diff --git a/database/migrations/2018_09_23_100608_create_album_default_anonymous_permissions_table.php b/database/migrations/2018_09_23_100608_create_album_default_anonymous_permissions_table.php new file mode 100644 index 0000000..7d1b51a --- /dev/null +++ b/database/migrations/2018_09_23_100608_create_album_default_anonymous_permissions_table.php @@ -0,0 +1,36 @@ +unsignedInteger('permission_id'); + + $table->foreign('permission_id') + ->references('id')->on('permissions') + ->onDelete('no action'); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('album_default_anonymous_permissions'); + } +} diff --git a/database/seeds/PermissionsSeeder.php b/database/seeds/PermissionsSeeder.php index 23a7df0..e5e5494 100644 --- a/database/seeds/PermissionsSeeder.php +++ b/database/seeds/PermissionsSeeder.php @@ -72,6 +72,14 @@ class PermissionsSeeder extends Seeder 'is_default' => false, 'sort_order' => 0 ]); + + // admin:manage-comments = controls if photo comments can be managed + DatabaseSeeder::createOrUpdate('permissions', [ + 'section' => 'admin', + 'description' => 'manage-comments', + 'is_default' => false, + 'sort_order' => 0 + ]); } private function seedAlbumPermissions() @@ -139,5 +147,21 @@ class PermissionsSeeder extends Seeder 'is_default' => false, 'sort_order' => 60 ]); + + // album:moderate-comments = moderate comments posted on photos + DatabaseSeeder::createOrUpdate('permissions', [ + 'section' => 'album', + 'description' => 'moderate-comments', + 'is_default' => false, + 'sort_order' => 70 + ]); + + // album:moderate-comments = moderate comments posted on photos + DatabaseSeeder::createOrUpdate('permissions', [ + 'section' => 'album', + 'description' => 'post-comment', + 'is_default' => false, + 'sort_order' => 80 + ]); } } diff --git a/public/css/blue-twilight.css b/public/css/blue-twilight.css index c8da95b..b9c96be 100644 --- a/public/css/blue-twilight.css +++ b/public/css/blue-twilight.css @@ -100,6 +100,10 @@ width: auto; } +.photo-comment .card-subtitle { + font-size: smaller; +} + .stats-table .icon-col { font-size: 1.4em; width: 20%; diff --git a/public/css/blue-twilight.min.css b/public/css/blue-twilight.min.css index 524a512..d61b69e 100644 --- a/public/css/blue-twilight.min.css +++ b/public/css/blue-twilight.min.css @@ -1,4 +1,4 @@ -.admin-sidebar-card{margin-bottom:15px}.album-expand-handle{cursor:pointer;margin-top:5px}.meta-label,.meta-value{vertical-align:middle !important}.photo .loading{background-color:#fff;display:none;height:100%;left:0;opacity:.8;position:absolute;text-align:center;top:0;width:100%;z-index:1000}.photo .loading img{margin-top:40px}.text-red{color:red}[v-cloak]{display:none}.activity-grid{font-size:smaller}.activity-grid th,.activity-grid td{padding:5px !important;text-align:center}.activity-grid td{color:#fff;height:20px}.activity-grid .has-activity{background-color:#1e90ff}.activity-grid .invalid-date{background-color:#e5e5e5}.activity-grid .no-activity{background-color:#fff}.activity-grid th:first-child,.activity-grid td:first-child{border-left:1px solid #dee2e6}.activity-grid th,.activity-grid td{border-right:1px solid #dee2e6}.activity-grid tr:last-child td{border-bottom:1px solid #dee2e6}.activity-grid .border-spacer-element{border-right-width:0;padding:0 !important;width:1px}.album-slideshow-container #image-preview{height:600px;max-width:100%;width:800px}.album-slideshow-container #image-preview img{max-width:100%}.album-slideshow-container .thumbnails{overflow-x:scroll;overflow-y:hidden;white-space:nowrap;width:auto}.stats-table .icon-col{font-size:1.4em;width:20%;vertical-align:middle}.stats-table .stat-col{font-size:1.8em;font-weight:bold;width:40%}.stats-table .text-col{font-size:1.2em;vertical-align:middle;width:40%}html{font-size:14px !important}button,input,optgroup,select,textarea{cursor:pointer;font-family:inherit !important}.album-photo-cards .card{margin-bottom:15px}.container,.container-fluid{margin-top:20px}.hidden{display:none}.tab-content{border:solid 1px #ddd;border-top:0;padding:20px}.tether-element,.tether-element:after,.tether-element:before,.tether-element *,.tether-element *:after,.tether-element *:before{box-sizing:border-box}.tether-element{position:absolute;display:none}.tether-element.tether-open{display:block}.tether-element.tether-theme-basic{max-width:100%;max-height:100%}.tether-element.tether-theme-basic .tether-content{border-radius:5px;box-shadow:0 2px 8px rgba(0,0,0,0.2);font-family:inherit;background:#fff;color:inherit;padding:1em;font-size:1.1em;line-height:1.5em}.tether-element,.tether-element:after,.tether-element:before,.tether-element *,.tether-element *:after,.tether-element *:before{box-sizing:border-box}.tether-element{position:absolute;display:none}.tether-element.tether-open{display:block}.tt-query{-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.tt-hint{color:#999}.tt-menu{width:422px;margin-top:4px;padding:4px 0;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.tt-suggestion{padding:3px 20px;line-height:24px}.tt-suggestion.tt-cursor,.tt-suggestion:hover{color:#fff;background-color:#0097cf}.tt-suggestion p{margin:0}/*! +.admin-sidebar-card{margin-bottom:15px}.album-expand-handle{cursor:pointer;margin-top:5px}.meta-label,.meta-value{vertical-align:middle !important}.photo .loading{background-color:#fff;display:none;height:100%;left:0;opacity:.8;position:absolute;text-align:center;top:0;width:100%;z-index:1000}.photo .loading img{margin-top:40px}.text-red{color:red}[v-cloak]{display:none}.activity-grid{font-size:smaller}.activity-grid th,.activity-grid td{padding:5px !important;text-align:center}.activity-grid td{color:#fff;height:20px}.activity-grid .has-activity{background-color:#1e90ff}.activity-grid .invalid-date{background-color:#e5e5e5}.activity-grid .no-activity{background-color:#fff}.activity-grid th:first-child,.activity-grid td:first-child{border-left:1px solid #dee2e6}.activity-grid th,.activity-grid td{border-right:1px solid #dee2e6}.activity-grid tr:last-child td{border-bottom:1px solid #dee2e6}.activity-grid .border-spacer-element{border-right-width:0;padding:0 !important;width:1px}.album-slideshow-container #image-preview{height:600px;max-width:100%;width:800px}.album-slideshow-container #image-preview img{max-width:100%}.album-slideshow-container .thumbnails{overflow-x:scroll;overflow-y:hidden;white-space:nowrap;width:auto}.photo-comment .card-subtitle{font-size:smaller}.stats-table .icon-col{font-size:1.4em;width:20%;vertical-align:middle}.stats-table .stat-col{font-size:1.8em;font-weight:bold;width:40%}.stats-table .text-col{font-size:1.2em;vertical-align:middle;width:40%}html{font-size:14px !important}button,input,optgroup,select,textarea{cursor:pointer;font-family:inherit !important}.album-photo-cards .card{margin-bottom:15px}.container,.container-fluid{margin-top:20px}.hidden{display:none}.tab-content{border:solid 1px #ddd;border-top:0;padding:20px}.tether-element,.tether-element:after,.tether-element:before,.tether-element *,.tether-element *:after,.tether-element *:before{box-sizing:border-box}.tether-element{position:absolute;display:none}.tether-element.tether-open{display:block}.tether-element.tether-theme-basic{max-width:100%;max-height:100%}.tether-element.tether-theme-basic .tether-content{border-radius:5px;box-shadow:0 2px 8px rgba(0,0,0,0.2);font-family:inherit;background:#fff;color:inherit;padding:1em;font-size:1.1em;line-height:1.5em}.tether-element,.tether-element:after,.tether-element:before,.tether-element *,.tether-element *:after,.tether-element *:before{box-sizing:border-box}.tether-element{position:absolute;display:none}.tether-element.tether-open{display:block}.tt-query{-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.tt-hint{color:#999}.tt-menu{width:422px;margin-top:4px;padding:4px 0;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.tt-suggestion{padding:3px 20px;line-height:24px}.tt-suggestion.tt-cursor,.tt-suggestion:hover{color:#fff;background-color:#0097cf}.tt-suggestion p{margin:0}/*! * Bootstrap v4.1.2 (https://getbootstrap.com/) * Copyright 2011-2018 The Bootstrap Authors * Copyright 2011-2018 Twitter, Inc. diff --git a/public/js/blue-twilight.js b/public/js/blue-twilight.js index 88ffc9e..272d0bb 100644 --- a/public/js/blue-twilight.js +++ b/public/js/blue-twilight.js @@ -41534,6 +41534,82 @@ module.exports = function(Chart) { },{"25":25,"45":45,"6":6}]},{},[7])(7) }); +/** + * This model is used by gallery/photo.blade.php, to handle comments and individual photo actions. + * @constructor + */ +function PhotoViewModel(urls) { + this.el = '#photo-app'; + + this.data = { + is_reply_form_loading: false, + reply_comment_id: 0 + }; + + this.computed = { + replyFormStyle: function() + { + return { + 'display': this.is_reply_form_loading ? 'none' : 'block' + }; + } + }; + + this.methods = { + init: function() { + var self = this; + + // Load the right comment reply form + $('#comment-reply-modal').on('show.bs.modal', function (event) { + var url = urls.reply_comment_form.replace(/-1$/, self.reply_comment_id); + $.get(url, function(result) + { + $('#comment-reply-form-content').html(result); + initTinyMce('#comment-text-reply'); + self.is_reply_form_loading = false; + }); + }); + + $('#comment-reply-modal').on('hide.bs.modal', function (event) { + tinymce.remove('#comment-text-reply'); + }); + }, + postCommentReply: function() { + var form = $('form', '#comment-reply-form-content'); + var formUrl = $(form).attr('action'); + + // Seems like the TinyMCE editor in the BS modal does not persist back to the textarea correctly - so do + // this manually (bit of a hack!) + $('#comment-text-reply', form).html(tinymce.get('comment-text-reply').getContent()); + + var formData = form.serialize(); + + $.post(formUrl, formData, function(result) + { + if (result.redirect_url) + { + window.location = result.redirect_url; + } + else + { + tinymce.remove('#comment-text-reply'); + $('#comment-reply-form-content').html(result); + initTinyMce('#comment-text-reply'); + } + }); + }, + replyToComment: function(e) { + var replyButton = $(e.target).closest('.photo-comment'); + this.reply_comment_id = replyButton.data('comment-id'); + + this.is_reply_form_loading = true; + $('#comment-reply-modal').modal('show'); + + e.preventDefault(); + return false; + } + }; +} function StorageLocationViewModel() { this.el = '#storage-options'; diff --git a/public/js/blue-twilight.min.js b/public/js/blue-twilight.min.js index 4b8105b..270e5b1 100644 --- a/public/js/blue-twilight.min.js +++ b/public/js/blue-twilight.min.js @@ -1 +1 @@ -function AboutViewModel(t){this.el="#about-app",this.data={can_upgrade:!1,is_loading:!0,version_body:"",version_date:"",version_name:"",version_url:""},this.computed={},this.methods={init:function(){var e=this;$.ajax(t.latest_release_url,{complete:function(){e.is_loading=!1},dataType:"json",error:function(t,e,n){},method:"GET",success:function(t){e.version_body=t.body,e.version_date=t.publish_date,e.version_name=t.name,e.version_url=t.url,e.can_upgrade=t.can_upgrade}})}}}function CreateAlbumViewModel(){this.el="#create-album-app",this.data={is_inherit_permissions:!0,is_private:!1,parent_id:""},this.computed={isParentAlbum:function(){return""==this.parent_id},isPrivateDisabled:function(){return!this.isParentAlbum&&this.is_inherit_permissions}}}function EditAlbumViewModel(){this.el="#edit-album-app",this.data={parent_id:""},this.computed={isParentAlbum:function(){return""==this.parent_id}}}function SettingsViewModel(t,e){this.el="#settings-app",this.data={is_rebuilding_permissions_cache:!1},this.methods={rebuildPermissionsCache:function(n){var i=this;return $.ajax(t.rebuild_permissions_cache,{complete:function(){i.is_rebuilding_permissions_cache=!1},dataType:"json",error:function(t,n,i){alert(e.permissions_cache_rebuild_failed)},method:"POST",success:function(t){alert(e.permissions_cache_rebuild_succeeded)}}),n.preventDefault(),!1}}}function AnalyseAlbumViewModel(){this.el="#analyse-album",this.data={imagesFailed:[],imagesToAnalyse:[],imagesInProgress:[],imagesRecentlyCompleted:[],numberSuccessful:0,numberFailed:0},this.computed={failedPercentage:function(){var t=0;return this.numberTotal>0&&(t=this.numberFailed/this.numberTotal*100),t.toFixed(2)+"%"},isCompleted:function(){return this.numberTotal>0&&this.numberSuccessful+this.numberFailed>=this.numberTotal},latestCompletedImages:function(){var t=this.imagesRecentlyCompleted.length-3<0?0:this.imagesRecentlyCompleted.length-3,e=t+3;return this.imagesRecentlyCompleted.slice(t,e)},numberTotal:function(){return this.imagesToAnalyse.length},successfulPercentage:function(){var t=0;return this.numberTotal>0&&(t=this.numberSuccessful/this.numberTotal*100),t.toFixed(2)+"%"}},this.methods={analyseImage:function(t){var e=this;this.imagesToAnalyse.push(t),$.ajax(t.url,{beforeSend:function(){e.imagesInProgress.push(t)},dataType:"json",error:function(n,i,r){e.numberFailed++,e.imagesFailed.push({name:t.name,reason:i}),t.isSuccessful=!1,t.isPending=!1},method:"POST",success:function(n){if(n.is_successful){e.numberSuccessful++,t.isSuccessful=!0,t.isPending=!1,e.imagesRecentlyCompleted.push(t);var i=e.imagesInProgress.indexOf(t);i>-1&&e.imagesInProgress.splice(i,1)}else e.numberFailed++,e.imagesFailed.push({name:t.name,reason:n.message}),t.isSuccessful=!1,t.isPending=!1}})}}}function AnalyseImageViewModel(t){this.isPending=!0,this.isSuccessful=!1,this.name=t.name,this.photoID=t.photo_id,this.url=t.url}function EditPhotosViewModel(t,e,n){this.el="#photos-tab",this.data={albums:[],bulkModifyMethod:"",isSubmitting:!1,photoIDs:[],photoIDsAvailable:[],selectAllInAlbum:0},this.methods={bulkModifySelected:function(t){if(this.isSubmitting)return!0;var n=this,i=$(t.target).closest("form");return"change_album"===this.bulkModifyMethod?(this.promptForNewAlbum(function(t){var e=$("select",t).val();$('input[name="new-album-id"]',i).val(e),n.isSubmitting=!0,$('button[name="bulk-apply"]',i).click(),_bt_showLoadingModal()}),t.preventDefault(),!1):"delete"!==this.bulkModifyMethod||(bootbox.dialog({message:e.delete_bulk_confirm_message,title:e.delete_bulk_confirm_title,buttons:{cancel:{label:e.action_cancel,className:"btn-secondary"},confirm:{label:e.action_delete,className:"btn-danger",callback:function(){n.isSubmitting=!0,$('button[name="bulk-apply"]',i).click(),_bt_showLoadingModal()}}}}),t.preventDefault(),!1)},changeAlbum:function(t){this.selectPhotoSingle(t.target);var e=this.photoIDs[0];return this.photoIDs=[],this.promptForNewAlbum(function(t){var i=$("select",t).val();$.post(n.move_photo.replace(/\/0$/,"/"+e),{new_album_id:i},function(){window.location.reload()})}),t.preventDefault(),!1},deletePhoto:function(t){this.selectPhotoSingle(t.target);var i=this.photoIDs[0];return this.photoIDs=[],bootbox.dialog({message:e.delete_confirm_message,title:e.delete_confirm_title,buttons:{cancel:{label:e.action_cancel,className:"btn-secondary"},confirm:{label:e.action_delete,className:"btn-danger",callback:function(){var t=n.delete_photo;t=t.replace(/\/0$/,"/"+i),$(".loading",parent).show(),$.post(t,{_method:"DELETE"},function(t){window.location.reload()})}}}}),t.preventDefault(),!1},flip:function(t,e,i){var r=n.flip_photo;r=r.replace("/0/","/"+this.photoIDs[0]+"/"),r=t?r.replace(/\/-1\//,"/1/"):r.replace(/\/-1\//,"/0/"),r=e?r.replace(/\/-2$/,"/1"):r.replace(/\/-2$/,"/0"),$(".loading",i).show(),$.post(r,function(){var t=$("img.photo-thumbnail",i),e=t.data("original-src");t.attr("src",e+"&_="+(new Date).getTime()),$(".loading",i).hide()}),this.photoIDs=[]},flipBoth:function(t){return this.selectPhotoSingle(t.target),this.flip(!0,!0,$(t.target).closest(".photo")),t.preventDefault(),!1},flipHorizontal:function(t){return this.selectPhotoSingle(t.target),this.flip(!0,!1,$(t.target).closest(".photo")),t.preventDefault(),!1},flipVertical:function(t){return this.selectPhotoSingle(t.target),this.flip(!1,!0,$(t.target).closest(".photo")),t.preventDefault(),!1},isPhotoSelected:function(t){return this.photoIDs.indexOf(t)>-1?"checked":""},promptForNewAlbum:function(n){for(var i=this.albums,r=$("",t.querySelectorAll("[msallowcapture^='']").length&&m.push("[*^$]="+L+"*(?:''|\"\")"),t.querySelectorAll("[selected]").length||m.push("\\["+L+"*(?:value|"+F+")"),t.querySelectorAll("[id~="+_+"-]").length||m.push("~="),t.querySelectorAll(":checked").length||m.push(":checked"),t.querySelectorAll("a#"+_+"+*").length||m.push(".#.+[+~]")}),lt(function(t){t.innerHTML="";var e=f.createElement("input");e.setAttribute("type","hidden"),t.appendChild(e).setAttribute("name","D"),t.querySelectorAll("[name=d]").length&&m.push("name"+L+"*[*^$|!~]?="),2!==t.querySelectorAll(":enabled").length&&m.push(":enabled",":disabled"),p.appendChild(t).disabled=!0,2!==t.querySelectorAll(":disabled").length&&m.push(":enabled",":disabled"),t.querySelectorAll("*,:x"),m.push(",.*:")})),(n.matchesSelector=Q.test(y=p.matches||p.webkitMatchesSelector||p.mozMatchesSelector||p.oMatchesSelector||p.msMatchesSelector))&<(function(t){n.disconnectedMatch=y.call(t,"*"),y.call(t,"[s!='']:x"),v.push("!=",j)}),m=m.length&&new RegExp(m.join("|")),v=v.length&&new RegExp(v.join("|")),e=Q.test(p.compareDocumentPosition),b=e||Q.test(p.contains)?function(t,e){var n=9===t.nodeType?t.documentElement:t,i=e&&e.parentNode;return t===i||!(!i||1!==i.nodeType||!(n.contains?n.contains(i):t.compareDocumentPosition&&16&t.compareDocumentPosition(i)))}:function(t,e){if(e)for(;e=e.parentNode;)if(e===t)return!0;return!1},D=e?function(t,e){if(t===e)return d=!0,0;var i=!t.compareDocumentPosition-!e.compareDocumentPosition;return i||(1&(i=(t.ownerDocument||t)===(e.ownerDocument||e)?t.compareDocumentPosition(e):1)||!n.sortDetached&&e.compareDocumentPosition(t)===i?t===f||t.ownerDocument===x&&b(x,t)?-1:e===f||e.ownerDocument===x&&b(x,e)?1:c?N(c,t)-N(c,e):0:4&i?-1:1)}:function(t,e){if(t===e)return d=!0,0;var n,i=0,r=t.parentNode,o=e.parentNode,a=[t],s=[e];if(!r||!o)return t===f?-1:e===f?1:r?-1:o?1:c?N(c,t)-N(c,e):0;if(r===o)return ct(t,e);for(n=t;n=n.parentNode;)a.unshift(n);for(n=e;n=n.parentNode;)s.unshift(n);for(;a[i]===s[i];)i++;return i?ct(a[i],s[i]):a[i]===x?-1:s[i]===x?1:0},f):f},ot.matches=function(t,e){return ot(t,null,null,e)},ot.matchesSelector=function(t,e){if((t.ownerDocument||t)!==f&&h(t),e=e.replace(q,"='$1']"),n.matchesSelector&&g&&!T[e+" "]&&(!v||!v.test(e))&&(!m||!m.test(e)))try{var i=y.call(t,e);if(i||n.disconnectedMatch||t.document&&11!==t.document.nodeType)return i}catch(t){}return ot(e,f,null,[t]).length>0},ot.contains=function(t,e){return(t.ownerDocument||t)!==f&&h(t),b(t,e)},ot.attr=function(t,e){(t.ownerDocument||t)!==f&&h(t);var r=i.attrHandle[e.toLowerCase()],o=r&&O.call(i.attrHandle,e.toLowerCase())?r(t,e,!g):void 0;return void 0!==o?o:n.attributes||!g?t.getAttribute(e):(o=t.getAttributeNode(e))&&o.specified?o.value:null},ot.escape=function(t){return(t+"").replace(et,nt)},ot.error=function(t){throw new Error("Syntax error, unrecognized expression: "+t)},ot.uniqueSort=function(t){var e,i=[],r=0,o=0;if(d=!n.detectDuplicates,c=!n.sortStable&&t.slice(0),t.sort(D),d){for(;e=t[o++];)e===t[o]&&(r=i.push(o));for(;r--;)t.splice(i[r],1)}return c=null,t},r=ot.getText=function(t){var e,n="",i=0,o=t.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof t.textContent)return t.textContent;for(t=t.firstChild;t;t=t.nextSibling)n+=r(t)}else if(3===o||4===o)return t.nodeValue}else for(;e=t[i++];)n+=r(e);return n},(i=ot.selectors={cacheLength:50,createPseudo:st,match:U,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(t){return t[1]=t[1].replace(Z,tt),t[3]=(t[3]||t[4]||t[5]||"").replace(Z,tt),"~="===t[2]&&(t[3]=" "+t[3]+" "),t.slice(0,4)},CHILD:function(t){return t[1]=t[1].toLowerCase(),"nth"===t[1].slice(0,3)?(t[3]||ot.error(t[0]),t[4]=+(t[4]?t[5]+(t[6]||1):2*("even"===t[3]||"odd"===t[3])),t[5]=+(t[7]+t[8]||"odd"===t[3])):t[3]&&ot.error(t[0]),t},PSEUDO:function(t){var e,n=!t[6]&&t[2];return U.CHILD.test(t[0])?null:(t[3]?t[2]=t[4]||t[5]||"":n&&V.test(n)&&(e=a(n,!0))&&(e=n.indexOf(")",n.length-e)-n.length)&&(t[0]=t[0].slice(0,e),t[2]=n.slice(0,e)),t.slice(0,3))}},filter:{TAG:function(t){var e=t.replace(Z,tt).toLowerCase();return"*"===t?function(){return!0}:function(t){return t.nodeName&&t.nodeName.toLowerCase()===e}},CLASS:function(t){var e=k[t+" "];return e||(e=new RegExp("(^|"+L+")"+t+"("+L+"|$)"))&&k(t,function(t){return e.test("string"==typeof t.className&&t.className||void 0!==t.getAttribute&&t.getAttribute("class")||"")})},ATTR:function(t,e,n){return function(i){var r=ot.attr(i,t);return null==r?"!="===e:!e||(r+="","="===e?r===n:"!="===e?r!==n:"^="===e?n&&0===r.indexOf(n):"*="===e?n&&r.indexOf(n)>-1:"$="===e?n&&r.slice(-n.length)===n:"~="===e?(" "+r.replace(H," ")+" ").indexOf(n)>-1:"|="===e&&(r===n||r.slice(0,n.length+1)===n+"-"))}},CHILD:function(t,e,n,i,r){var o="nth"!==t.slice(0,3),a="last"!==t.slice(-4),s="of-type"===e;return 1===i&&0===r?function(t){return!!t.parentNode}:function(e,n,l){var u,c,d,h,f,p,g=o!==a?"nextSibling":"previousSibling",m=e.parentNode,v=s&&e.nodeName.toLowerCase(),y=!l&&!s,b=!1;if(m){if(o){for(;g;){for(h=e;h=h[g];)if(s?h.nodeName.toLowerCase()===v:1===h.nodeType)return!1;p=g="only"===t&&!p&&"nextSibling"}return!0}if(p=[a?m.firstChild:m.lastChild],a&&y){for(b=(f=(u=(c=(d=(h=m)[_]||(h[_]={}))[h.uniqueID]||(d[h.uniqueID]={}))[t]||[])[0]===w&&u[1])&&u[2],h=f&&m.childNodes[f];h=++f&&h&&h[g]||(b=f=0)||p.pop();)if(1===h.nodeType&&++b&&h===e){c[t]=[w,f,b];break}}else if(y&&(b=f=(u=(c=(d=(h=e)[_]||(h[_]={}))[h.uniqueID]||(d[h.uniqueID]={}))[t]||[])[0]===w&&u[1]),!1===b)for(;(h=++f&&h&&h[g]||(b=f=0)||p.pop())&&((s?h.nodeName.toLowerCase()!==v:1!==h.nodeType)||!++b||(y&&((c=(d=h[_]||(h[_]={}))[h.uniqueID]||(d[h.uniqueID]={}))[t]=[w,b]),h!==e)););return(b-=r)===i||b%i==0&&b/i>=0}}},PSEUDO:function(t,e){var n,r=i.pseudos[t]||i.setFilters[t.toLowerCase()]||ot.error("unsupported pseudo: "+t);return r[_]?r(e):r.length>1?(n=[t,t,"",e],i.setFilters.hasOwnProperty(t.toLowerCase())?st(function(t,n){for(var i,o=r(t,e),a=o.length;a--;)t[i=N(t,o[a])]=!(n[i]=o[a])}):function(t){return r(t,0,n)}):r}},pseudos:{not:st(function(t){var e=[],n=[],i=s(t.replace(W,"$1"));return i[_]?st(function(t,e,n,r){for(var o,a=i(t,null,r,[]),s=t.length;s--;)(o=a[s])&&(t[s]=!(e[s]=o))}):function(t,r,o){return e[0]=t,i(e,null,o,n),e[0]=null,!n.pop()}}),has:st(function(t){return function(e){return ot(t,e).length>0}}),contains:st(function(t){return t=t.replace(Z,tt),function(e){return(e.textContent||e.innerText||r(e)).indexOf(t)>-1}}),lang:st(function(t){return Y.test(t||"")||ot.error("unsupported lang: "+t),t=t.replace(Z,tt).toLowerCase(),function(e){var n;do{if(n=g?e.lang:e.getAttribute("xml:lang")||e.getAttribute("lang"))return(n=n.toLowerCase())===t||0===n.indexOf(t+"-")}while((e=e.parentNode)&&1===e.nodeType);return!1}}),target:function(e){var n=t.location&&t.location.hash;return n&&n.slice(1)===e.id},root:function(t){return t===p},focus:function(t){return t===f.activeElement&&(!f.hasFocus||f.hasFocus())&&!!(t.type||t.href||~t.tabIndex)},enabled:ft(!1),disabled:ft(!0),checked:function(t){var e=t.nodeName.toLowerCase();return"input"===e&&!!t.checked||"option"===e&&!!t.selected},selected:function(t){return t.parentNode&&t.parentNode.selectedIndex,!0===t.selected},empty:function(t){for(t=t.firstChild;t;t=t.nextSibling)if(t.nodeType<6)return!1;return!0},parent:function(t){return!i.pseudos.empty(t)},header:function(t){return G.test(t.nodeName)},input:function(t){return K.test(t.nodeName)},button:function(t){var e=t.nodeName.toLowerCase();return"input"===e&&"button"===t.type||"button"===e},text:function(t){var e;return"input"===t.nodeName.toLowerCase()&&"text"===t.type&&(null==(e=t.getAttribute("type"))||"text"===e.toLowerCase())},first:pt(function(){return[0]}),last:pt(function(t,e){return[e-1]}),eq:pt(function(t,e,n){return[n<0?n+e:n]}),even:pt(function(t,e){for(var n=0;n=0;)t.push(i);return t}),gt:pt(function(t,e,n){for(var i=n<0?n+e:n;++i1?function(e,n,i){for(var r=t.length;r--;)if(!t[r](e,n,i))return!1;return!0}:t[0]}function _t(t,e,n,i,r){for(var o,a=[],s=0,l=t.length,u=null!=e;s-1&&(o[u]=!(a[u]=d))}}else v=_t(v===a?v.splice(p,v.length):v),r?r(null,a,v,l):I.apply(a,v)})}function wt(t){for(var e,n,r,o=t.length,a=i.relative[t[0].type],s=a||i.relative[" "],l=a?1:0,c=yt(function(t){return t===e},s,!0),d=yt(function(t){return N(e,t)>-1},s,!0),h=[function(t,n,i){var r=!a&&(i||n!==u)||((e=n).nodeType?c(t,n,i):d(t,n,i));return e=null,r}];l1&&bt(h),l>1&&vt(t.slice(0,l-1).concat({value:" "===t[l-2].type?"*":""})).replace(W,"$1"),n,l0,r=t.length>0,o=function(o,a,s,l,c){var d,p,m,v=0,y="0",b=o&&[],_=[],x=u,C=o||r&&i.find.TAG("*",c),k=w+=null==x?1:Math.random()||.1,S=C.length;for(c&&(u=a===f||a||c);y!==S&&null!=(d=C[y]);y++){if(r&&d){for(p=0,a||d.ownerDocument===f||(h(d),s=!g);m=t[p++];)if(m(d,a||f,s)){l.push(d);break}c&&(w=k)}n&&((d=!m&&d)&&v--,o&&b.push(d))}if(v+=y,n&&y!==v){for(p=0;m=e[p++];)m(b,_,a,s);if(o){if(v>0)for(;y--;)b[y]||_[y]||(_[y]=M.call(l));_=_t(_)}I.apply(l,_),c&&!o&&_.length>0&&v+e.length>1&&ot.uniqueSort(l)}return c&&(w=k,u=x),b};return n?st(o):o}(o,r))).selector=t}return s},l=ot.select=function(t,e,n,r){var o,l,u,c,d,h="function"==typeof t&&t,f=!r&&a(t=h.selector||t);if(n=n||[],1===f.length){if((l=f[0]=f[0].slice(0)).length>2&&"ID"===(u=l[0]).type&&9===e.nodeType&&g&&i.relative[l[1].type]){if(!(e=(i.find.ID(u.matches[0].replace(Z,tt),e)||[])[0]))return n;h&&(e=e.parentNode),t=t.slice(l.shift().value.length)}for(o=U.needsContext.test(t)?0:l.length;o--&&(u=l[o],!i.relative[c=u.type]);)if((d=i.find[c])&&(r=d(u.matches[0].replace(Z,tt),J.test(l[0].type)&>(e.parentNode)||e))){if(l.splice(o,1),!(t=r.length&&vt(l)))return I.apply(n,r),n;break}}return(h||s(t,f))(r,e,!g,n,!e||J.test(t)&>(e.parentNode)||e),n},n.sortStable=_.split("").sort(D).join("")===_,n.detectDuplicates=!!d,h(),n.sortDetached=lt(function(t){return 1&t.compareDocumentPosition(f.createElement("fieldset"))}),lt(function(t){return t.innerHTML="","#"===t.firstChild.getAttribute("href")})||ut("type|href|height|width",function(t,e,n){if(!n)return t.getAttribute(e,"type"===e.toLowerCase()?1:2)}),n.attributes&<(function(t){return t.innerHTML="",t.firstChild.setAttribute("value",""),""===t.firstChild.getAttribute("value")})||ut("value",function(t,e,n){if(!n&&"input"===t.nodeName.toLowerCase())return t.defaultValue}),lt(function(t){return null==t.getAttribute("disabled")})||ut(F,function(t,e,n){var i;if(!n)return!0===t[e]?e.toLowerCase():(i=t.getAttributeNode(e))&&i.specified?i.value:null}),ot}(t);m.find=w,m.expr=w.selectors,m.expr[":"]=m.expr.pseudos,m.uniqueSort=m.unique=w.uniqueSort,m.text=w.getText,m.isXMLDoc=w.isXML,m.contains=w.contains,m.escapeSelector=w.escape;var C=function(t,e,n){for(var i=[],r=void 0!==n;(t=t[e])&&9!==t.nodeType;)if(1===t.nodeType){if(r&&m(t).is(n))break;i.push(t)}return i},k=function(t,e){for(var n=[];t;t=t.nextSibling)1===t.nodeType&&t!==e&&n.push(t);return n},S=m.expr.match.needsContext;function T(t,e){return t.nodeName&&t.nodeName.toLowerCase()===e.toLowerCase()}var D=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,O=/^.[^:#\[\.,]*$/;function A(t,e,n){return m.isFunction(e)?m.grep(t,function(t,i){return!!e.call(t,i,t)!==n}):e.nodeType?m.grep(t,function(t){return t===e!==n}):"string"!=typeof e?m.grep(t,function(t){return l.call(e,t)>-1!==n}):O.test(e)?m.filter(e,t,n):(e=m.filter(e,t),m.grep(t,function(t){return l.call(e,t)>-1!==n&&1===t.nodeType}))}m.filter=function(t,e,n){var i=e[0];return n&&(t=":not("+t+")"),1===e.length&&1===i.nodeType?m.find.matchesSelector(i,t)?[i]:[]:m.find.matches(t,m.grep(e,function(t){return 1===t.nodeType}))},m.fn.extend({find:function(t){var e,n,i=this.length,r=this;if("string"!=typeof t)return this.pushStack(m(t).filter(function(){for(e=0;e1?m.uniqueSort(n):n},filter:function(t){return this.pushStack(A(this,t||[],!1))},not:function(t){return this.pushStack(A(this,t||[],!0))},is:function(t){return!!A(this,"string"==typeof t&&S.test(t)?m(t):t||[],!1).length}});var M,E=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(m.fn.init=function(t,e,n){var r,o;if(!t)return this;if(n=n||M,"string"==typeof t){if(!(r="<"===t[0]&&">"===t[t.length-1]&&t.length>=3?[null,t,null]:E.exec(t))||!r[1]&&e)return!e||e.jquery?(e||n).find(t):this.constructor(e).find(t);if(r[1]){if(e=e instanceof m?e[0]:e,m.merge(this,m.parseHTML(r[1],e&&e.nodeType?e.ownerDocument||e:i,!0)),D.test(r[1])&&m.isPlainObject(e))for(r in e)m.isFunction(this[r])?this[r](e[r]):this.attr(r,e[r]);return this}return(o=i.getElementById(r[2]))&&(this[0]=o,this.length=1),this}return t.nodeType?(this[0]=t,this.length=1,this):m.isFunction(t)?void 0!==n.ready?n.ready(t):t(m):m.makeArray(t,this)}).prototype=m.fn,M=m(i);var I=/^(?:parents|prev(?:Until|All))/,P={children:!0,contents:!0,next:!0,prev:!0};function N(t,e){for(;(t=t[e])&&1!==t.nodeType;);return t}m.fn.extend({has:function(t){var e=m(t,this),n=e.length;return this.filter(function(){for(var t=0;t-1:1===n.nodeType&&m.find.matchesSelector(n,t))){o.push(n);break}return this.pushStack(o.length>1?m.uniqueSort(o):o)},index:function(t){return t?"string"==typeof t?l.call(m(t),this[0]):l.call(this,t.jquery?t[0]:t):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(t,e){return this.pushStack(m.uniqueSort(m.merge(this.get(),m(t,e))))},addBack:function(t){return this.add(null==t?this.prevObject:this.prevObject.filter(t))}}),m.each({parent:function(t){var e=t.parentNode;return e&&11!==e.nodeType?e:null},parents:function(t){return C(t,"parentNode")},parentsUntil:function(t,e,n){return C(t,"parentNode",n)},next:function(t){return N(t,"nextSibling")},prev:function(t){return N(t,"previousSibling")},nextAll:function(t){return C(t,"nextSibling")},prevAll:function(t){return C(t,"previousSibling")},nextUntil:function(t,e,n){return C(t,"nextSibling",n)},prevUntil:function(t,e,n){return C(t,"previousSibling",n)},siblings:function(t){return k((t.parentNode||{}).firstChild,t)},children:function(t){return k(t.firstChild)},contents:function(t){return T(t,"iframe")?t.contentDocument:(T(t,"template")&&(t=t.content||t),m.merge([],t.childNodes))}},function(t,e){m.fn[t]=function(n,i){var r=m.map(this,e,n);return"Until"!==t.slice(-5)&&(i=n),i&&"string"==typeof i&&(r=m.filter(i,r)),this.length>1&&(P[t]||m.uniqueSort(r),I.test(t)&&r.reverse()),this.pushStack(r)}});var F=/[^\x20\t\r\n\f]+/g;function L(t){return t}function R(t){throw t}function $(t,e,n,i){var r;try{t&&m.isFunction(r=t.promise)?r.call(t).done(e).fail(n):t&&m.isFunction(r=t.then)?r.call(t,e,n):e.apply(void 0,[t].slice(i))}catch(t){n.apply(void 0,[t])}}m.Callbacks=function(t){t="string"==typeof t?function(t){var e={};return m.each(t.match(F)||[],function(t,n){e[n]=!0}),e}(t):m.extend({},t);var e,n,i,r,o=[],a=[],s=-1,l=function(){for(r=r||t.once,i=e=!0;a.length;s=-1)for(n=a.shift();++s-1;)o.splice(n,1),n<=s&&s--}),this},has:function(t){return t?m.inArray(t,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return r=a=[],o=n="",this},disabled:function(){return!o},lock:function(){return r=a=[],n||e||(o=n=""),this},locked:function(){return!!r},fireWith:function(t,n){return r||(n=[t,(n=n||[]).slice?n.slice():n],a.push(n),e||l()),this},fire:function(){return u.fireWith(this,arguments),this},fired:function(){return!!i}};return u},m.extend({Deferred:function(e){var n=[["notify","progress",m.Callbacks("memory"),m.Callbacks("memory"),2],["resolve","done",m.Callbacks("once memory"),m.Callbacks("once memory"),0,"resolved"],["reject","fail",m.Callbacks("once memory"),m.Callbacks("once memory"),1,"rejected"]],i="pending",r={state:function(){return i},always:function(){return o.done(arguments).fail(arguments),this},catch:function(t){return r.then(null,t)},pipe:function(){var t=arguments;return m.Deferred(function(e){m.each(n,function(n,i){var r=m.isFunction(t[i[4]])&&t[i[4]];o[i[1]](function(){var t=r&&r.apply(this,arguments);t&&m.isFunction(t.promise)?t.promise().progress(e.notify).done(e.resolve).fail(e.reject):e[i[0]+"With"](this,r?[t]:arguments)})}),t=null}).promise()},then:function(e,i,r){var o=0;function a(e,n,i,r){return function(){var s=this,l=arguments,u=function(){var t,u;if(!(e=o&&(i!==R&&(s=void 0,l=[t]),n.rejectWith(s,l))}};e?c():(m.Deferred.getStackHook&&(c.stackTrace=m.Deferred.getStackHook()),t.setTimeout(c))}}return m.Deferred(function(t){n[0][3].add(a(0,t,m.isFunction(r)?r:L,t.notifyWith)),n[1][3].add(a(0,t,m.isFunction(e)?e:L)),n[2][3].add(a(0,t,m.isFunction(i)?i:R))}).promise()},promise:function(t){return null!=t?m.extend(t,r):r}},o={};return m.each(n,function(t,e){var a=e[2],s=e[5];r[e[1]]=a.add,s&&a.add(function(){i=s},n[3-t][2].disable,n[0][2].lock),a.add(e[3].fire),o[e[0]]=function(){return o[e[0]+"With"](this===o?void 0:this,arguments),this},o[e[0]+"With"]=a.fireWith}),r.promise(o),e&&e.call(o,o),o},when:function(t){var e=arguments.length,n=e,i=Array(n),r=o.call(arguments),a=m.Deferred(),s=function(t){return function(n){i[t]=this,r[t]=arguments.length>1?o.call(arguments):n,--e||a.resolveWith(i,r)}};if(e<=1&&($(t,a.done(s(n)).resolve,a.reject,!e),"pending"===a.state()||m.isFunction(r[n]&&r[n].then)))return a.then();for(;n--;)$(r[n],s(n),a.reject);return a.promise()}});var j=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;m.Deferred.exceptionHook=function(e,n){t.console&&t.console.warn&&e&&j.test(e.name)&&t.console.warn("jQuery.Deferred exception: "+e.message,e.stack,n)},m.readyException=function(e){t.setTimeout(function(){throw e})};var H=m.Deferred();function W(){i.removeEventListener("DOMContentLoaded",W),t.removeEventListener("load",W),m.ready()}m.fn.ready=function(t){return H.then(t).catch(function(t){m.readyException(t)}),this},m.extend({isReady:!1,readyWait:1,ready:function(t){(!0===t?--m.readyWait:m.isReady)||(m.isReady=!0,!0!==t&&--m.readyWait>0||H.resolveWith(i,[m]))}}),m.ready.then=H.then,"complete"===i.readyState||"loading"!==i.readyState&&!i.documentElement.doScroll?t.setTimeout(m.ready):(i.addEventListener("DOMContentLoaded",W),t.addEventListener("load",W));var B=function(t,e,n,i,r,o,a){var s=0,l=t.length,u=null==n;if("object"===m.type(n))for(s in r=!0,n)B(t,e,s,n[s],!0,o,a);else if(void 0!==i&&(r=!0,m.isFunction(i)||(a=!0),u&&(a?(e.call(t,i),e=null):(u=e,e=function(t,e,n){return u.call(m(t),n)})),e))for(;s1,null,!0)},removeData:function(t){return this.each(function(){Y.remove(this,t)})}}),m.extend({queue:function(t,e,n){var i;if(t)return e=(e||"fx")+"queue",i=V.get(t,e),n&&(!i||Array.isArray(n)?i=V.access(t,e,m.makeArray(n)):i.push(n)),i||[]},dequeue:function(t,e){e=e||"fx";var n=m.queue(t,e),i=n.length,r=n.shift(),o=m._queueHooks(t,e);"inprogress"===r&&(r=n.shift(),i--),r&&("fx"===e&&n.unshift("inprogress"),delete o.stop,r.call(t,function(){m.dequeue(t,e)},o)),!i&&o&&o.empty.fire()},_queueHooks:function(t,e){var n=e+"queueHooks";return V.get(t,n)||V.access(t,n,{empty:m.Callbacks("once memory").add(function(){V.remove(t,[e+"queue",n])})})}}),m.fn.extend({queue:function(t,e){var n=2;return"string"!=typeof t&&(e=t,t="fx",n--),arguments.length\x20\t\r\n\f]+)/i,st=/^$|\/(?:java|ecma)script/i,lt={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ut(t,e){var n;return n=void 0!==t.getElementsByTagName?t.getElementsByTagName(e||"*"):void 0!==t.querySelectorAll?t.querySelectorAll(e||"*"):[],void 0===e||e&&T(t,e)?m.merge([t],n):n}function ct(t,e){for(var n=0,i=t.length;n-1)r&&r.push(o);else if(u=m.contains(o.ownerDocument,o),a=ut(d.appendChild(o),"script"),u&&ct(a),n)for(c=0;o=a[c++];)st.test(o.type||"")&&n.push(o);return d}dt=i.createDocumentFragment().appendChild(i.createElement("div")),(ht=i.createElement("input")).setAttribute("type","radio"),ht.setAttribute("checked","checked"),ht.setAttribute("name","t"),dt.appendChild(ht),p.checkClone=dt.cloneNode(!0).cloneNode(!0).lastChild.checked,dt.innerHTML="",p.noCloneChecked=!!dt.cloneNode(!0).lastChild.defaultValue;var gt=i.documentElement,mt=/^key/,vt=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,yt=/^([^.]*)(?:\.(.+)|)/;function bt(){return!0}function _t(){return!1}function xt(){try{return i.activeElement}catch(t){}}function wt(t,e,n,i,r,o){var a,s;if("object"==typeof e){for(s in"string"!=typeof n&&(i=i||n,n=void 0),e)wt(t,s,n,i,e[s],o);return t}if(null==i&&null==r?(r=n,i=n=void 0):null==r&&("string"==typeof n?(r=i,i=void 0):(r=i,i=n,n=void 0)),!1===r)r=_t;else if(!r)return t;return 1===o&&(a=r,(r=function(t){return m().off(t),a.apply(this,arguments)}).guid=a.guid||(a.guid=m.guid++)),t.each(function(){m.event.add(this,e,r,i,n)})}m.event={global:{},add:function(t,e,n,i,r){var o,a,s,l,u,c,d,h,f,p,g,v=V.get(t);if(v)for(n.handler&&(n=(o=n).handler,r=o.selector),r&&m.find.matchesSelector(gt,r),n.guid||(n.guid=m.guid++),(l=v.events)||(l=v.events={}),(a=v.handle)||(a=v.handle=function(e){return void 0!==m&&m.event.triggered!==e.type?m.event.dispatch.apply(t,arguments):void 0}),u=(e=(e||"").match(F)||[""]).length;u--;)f=g=(s=yt.exec(e[u])||[])[1],p=(s[2]||"").split(".").sort(),f&&(d=m.event.special[f]||{},f=(r?d.delegateType:d.bindType)||f,d=m.event.special[f]||{},c=m.extend({type:f,origType:g,data:i,handler:n,guid:n.guid,selector:r,needsContext:r&&m.expr.match.needsContext.test(r),namespace:p.join(".")},o),(h=l[f])||((h=l[f]=[]).delegateCount=0,d.setup&&!1!==d.setup.call(t,i,p,a)||t.addEventListener&&t.addEventListener(f,a)),d.add&&(d.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),r?h.splice(h.delegateCount++,0,c):h.push(c),m.event.global[f]=!0)},remove:function(t,e,n,i,r){var o,a,s,l,u,c,d,h,f,p,g,v=V.hasData(t)&&V.get(t);if(v&&(l=v.events)){for(u=(e=(e||"").match(F)||[""]).length;u--;)if(f=g=(s=yt.exec(e[u])||[])[1],p=(s[2]||"").split(".").sort(),f){for(d=m.event.special[f]||{},h=l[f=(i?d.delegateType:d.bindType)||f]||[],s=s[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=h.length;o--;)c=h[o],!r&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||i&&i!==c.selector&&("**"!==i||!c.selector)||(h.splice(o,1),c.selector&&h.delegateCount--,d.remove&&d.remove.call(t,c));a&&!h.length&&(d.teardown&&!1!==d.teardown.call(t,p,v.handle)||m.removeEvent(t,f,v.handle),delete l[f])}else for(f in l)m.event.remove(t,f+e[u],n,i,!0);m.isEmptyObject(l)&&V.remove(t,"handle events")}},dispatch:function(t){var e,n,i,r,o,a,s=m.event.fix(t),l=new Array(arguments.length),u=(V.get(this,"events")||{})[s.type]||[],c=m.event.special[s.type]||{};for(l[0]=s,e=1;e=1))for(;u!==this;u=u.parentNode||this)if(1===u.nodeType&&("click"!==t.type||!0!==u.disabled)){for(o=[],a={},n=0;n-1:m.find(r,this,null,[u]).length),a[r]&&o.push(i);o.length&&s.push({elem:u,handlers:o})}return u=this,l\x20\t\r\n\f]*)[^>]*)\/>/gi,kt=/\s*$/g;function Ot(t,e){return T(t,"table")&&T(11!==e.nodeType?e:e.firstChild,"tr")&&m(">tbody",t)[0]||t}function At(t){return t.type=(null!==t.getAttribute("type"))+"/"+t.type,t}function Mt(t){var e=Tt.exec(t.type);return e?t.type=e[1]:t.removeAttribute("type"),t}function Et(t,e){var n,i,r,o,a,s,l,u;if(1===e.nodeType){if(V.hasData(t)&&(o=V.access(t),a=V.set(e,o),u=o.events))for(r in delete a.handle,a.events={},u)for(n=0,i=u[r].length;n1&&"string"==typeof v&&!p.checkClone&&St.test(v))return t.each(function(r){var o=t.eq(r);y&&(e[0]=v.call(this,r,o.html())),It(o,e,n,i)});if(h&&(o=(r=pt(e,t[0].ownerDocument,!1,t,i)).firstChild,1===r.childNodes.length&&(r=o),o||i)){for(l=(s=m.map(ut(r,"script"),At)).length;d")},clone:function(t,e,n){var i,r,o,a,s,l,u,c=t.cloneNode(!0),d=m.contains(t.ownerDocument,t);if(!(p.noCloneChecked||1!==t.nodeType&&11!==t.nodeType||m.isXMLDoc(t)))for(a=ut(c),i=0,r=(o=ut(t)).length;i0&&ct(a,!d&&ut(t,"script")),c},cleanData:function(t){for(var e,n,i,r=m.event.special,o=0;void 0!==(n=t[o]);o++)if(z(n)){if(e=n[V.expando]){if(e.events)for(i in e.events)r[i]?m.event.remove(n,i):m.removeEvent(n,i,e.handle);n[V.expando]=void 0}n[Y.expando]&&(n[Y.expando]=void 0)}}}),m.fn.extend({detach:function(t){return Pt(this,t,!0)},remove:function(t){return Pt(this,t)},text:function(t){return B(this,function(t){return void 0===t?m.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=t)})},null,t,arguments.length)},append:function(){return It(this,arguments,function(t){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Ot(this,t).appendChild(t)})},prepend:function(){return It(this,arguments,function(t){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var e=Ot(this,t);e.insertBefore(t,e.firstChild)}})},before:function(){return It(this,arguments,function(t){this.parentNode&&this.parentNode.insertBefore(t,this)})},after:function(){return It(this,arguments,function(t){this.parentNode&&this.parentNode.insertBefore(t,this.nextSibling)})},empty:function(){for(var t,e=0;null!=(t=this[e]);e++)1===t.nodeType&&(m.cleanData(ut(t,!1)),t.textContent="");return this},clone:function(t,e){return t=null!=t&&t,e=null==e?t:e,this.map(function(){return m.clone(this,t,e)})},html:function(t){return B(this,function(t){var e=this[0]||{},n=0,i=this.length;if(void 0===t&&1===e.nodeType)return e.innerHTML;if("string"==typeof t&&!kt.test(t)&&!lt[(at.exec(t)||["",""])[1].toLowerCase()]){t=m.htmlPrefilter(t);try{for(;n1)}}),m.Tween=Gt,Gt.prototype={constructor:Gt,init:function(t,e,n,i,r,o){this.elem=t,this.prop=n,this.easing=r||m.easing._default,this.options=e,this.start=this.now=this.cur(),this.end=i,this.unit=o||(m.cssNumber[n]?"":"px")},cur:function(){var t=Gt.propHooks[this.prop];return t&&t.get?t.get(this):Gt.propHooks._default.get(this)},run:function(t){var e,n=Gt.propHooks[this.prop];return this.options.duration?this.pos=e=m.easing[this.easing](t,this.options.duration*t,0,1,this.options.duration):this.pos=e=t,this.now=(this.end-this.start)*e+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):Gt.propHooks._default.set(this),this}},Gt.prototype.init.prototype=Gt.prototype,Gt.propHooks={_default:{get:function(t){var e;return 1!==t.elem.nodeType||null!=t.elem[t.prop]&&null==t.elem.style[t.prop]?t.elem[t.prop]:(e=m.css(t.elem,t.prop,""))&&"auto"!==e?e:0},set:function(t){m.fx.step[t.prop]?m.fx.step[t.prop](t):1!==t.elem.nodeType||null==t.elem.style[m.cssProps[t.prop]]&&!m.cssHooks[t.prop]?t.elem[t.prop]=t.now:m.style(t.elem,t.prop,t.now+t.unit)}}},Gt.propHooks.scrollTop=Gt.propHooks.scrollLeft={set:function(t){t.elem.nodeType&&t.elem.parentNode&&(t.elem[t.prop]=t.now)}},m.easing={linear:function(t){return t},swing:function(t){return.5-Math.cos(t*Math.PI)/2},_default:"swing"},m.fx=Gt.prototype.init,m.fx.step={};var Qt,Xt,Jt=/^(?:toggle|show|hide)$/,Zt=/queueHooks$/;function te(){Xt&&(!1===i.hidden&&t.requestAnimationFrame?t.requestAnimationFrame(te):t.setTimeout(te,m.fx.interval),m.fx.tick())}function ee(){return t.setTimeout(function(){Qt=void 0}),Qt=m.now()}function ne(t,e){var n,i=0,r={height:t};for(e=e?1:0;i<4;i+=2-e)r["margin"+(n=J[i])]=r["padding"+n]=t;return e&&(r.opacity=r.width=t),r}function ie(t,e,n){for(var i,r=(re.tweeners[e]||[]).concat(re.tweeners["*"]),o=0,a=r.length;o1)},removeAttr:function(t){return this.each(function(){m.removeAttr(this,t)})}}),m.extend({attr:function(t,e,n){var i,r,o=t.nodeType;if(3!==o&&8!==o&&2!==o)return void 0===t.getAttribute?m.prop(t,e,n):(1===o&&m.isXMLDoc(t)||(r=m.attrHooks[e.toLowerCase()]||(m.expr.match.bool.test(e)?oe:void 0)),void 0!==n?null===n?void m.removeAttr(t,e):r&&"set"in r&&void 0!==(i=r.set(t,n,e))?i:(t.setAttribute(e,n+""),n):r&&"get"in r&&null!==(i=r.get(t,e))?i:null==(i=m.find.attr(t,e))?void 0:i)},attrHooks:{type:{set:function(t,e){if(!p.radioValue&&"radio"===e&&T(t,"input")){var n=t.value;return t.setAttribute("type",e),n&&(t.value=n),e}}}},removeAttr:function(t,e){var n,i=0,r=e&&e.match(F);if(r&&1===t.nodeType)for(;n=r[i++];)t.removeAttribute(n)}}),oe={set:function(t,e,n){return!1===e?m.removeAttr(t,n):t.setAttribute(n,n),n}},m.each(m.expr.match.bool.source.match(/\w+/g),function(t,e){var n=ae[e]||m.find.attr;ae[e]=function(t,e,i){var r,o,a=e.toLowerCase();return i||(o=ae[a],ae[a]=r,r=null!=n(t,e,i)?a:null,ae[a]=o),r}});var se=/^(?:input|select|textarea|button)$/i,le=/^(?:a|area)$/i;function ue(t){return(t.match(F)||[]).join(" ")}function ce(t){return t.getAttribute&&t.getAttribute("class")||""}m.fn.extend({prop:function(t,e){return B(this,m.prop,t,e,arguments.length>1)},removeProp:function(t){return this.each(function(){delete this[m.propFix[t]||t]})}}),m.extend({prop:function(t,e,n){var i,r,o=t.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&m.isXMLDoc(t)||(e=m.propFix[e]||e,r=m.propHooks[e]),void 0!==n?r&&"set"in r&&void 0!==(i=r.set(t,n,e))?i:t[e]=n:r&&"get"in r&&null!==(i=r.get(t,e))?i:t[e]},propHooks:{tabIndex:{get:function(t){var e=m.find.attr(t,"tabindex");return e?parseInt(e,10):se.test(t.nodeName)||le.test(t.nodeName)&&t.href?0:-1}}},propFix:{for:"htmlFor",class:"className"}}),p.optSelected||(m.propHooks.selected={get:function(t){var e=t.parentNode;return e&&e.parentNode&&e.parentNode.selectedIndex,null},set:function(t){var e=t.parentNode;e&&(e.selectedIndex,e.parentNode&&e.parentNode.selectedIndex)}}),m.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){m.propFix[this.toLowerCase()]=this}),m.fn.extend({addClass:function(t){var e,n,i,r,o,a,s,l=0;if(m.isFunction(t))return this.each(function(e){m(this).addClass(t.call(this,e,ce(this)))});if("string"==typeof t&&t)for(e=t.match(F)||[];n=this[l++];)if(r=ce(n),i=1===n.nodeType&&" "+ue(r)+" "){for(a=0;o=e[a++];)i.indexOf(" "+o+" ")<0&&(i+=o+" ");r!==(s=ue(i))&&n.setAttribute("class",s)}return this},removeClass:function(t){var e,n,i,r,o,a,s,l=0;if(m.isFunction(t))return this.each(function(e){m(this).removeClass(t.call(this,e,ce(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof t&&t)for(e=t.match(F)||[];n=this[l++];)if(r=ce(n),i=1===n.nodeType&&" "+ue(r)+" "){for(a=0;o=e[a++];)for(;i.indexOf(" "+o+" ")>-1;)i=i.replace(" "+o+" "," ");r!==(s=ue(i))&&n.setAttribute("class",s)}return this},toggleClass:function(t,e){var n=typeof t;return"boolean"==typeof e&&"string"===n?e?this.addClass(t):this.removeClass(t):m.isFunction(t)?this.each(function(n){m(this).toggleClass(t.call(this,n,ce(this),e),e)}):this.each(function(){var e,i,r,o;if("string"===n)for(i=0,r=m(this),o=t.match(F)||[];e=o[i++];)r.hasClass(e)?r.removeClass(e):r.addClass(e);else void 0!==t&&"boolean"!==n||((e=ce(this))&&V.set(this,"__className__",e),this.setAttribute&&this.setAttribute("class",e||!1===t?"":V.get(this,"__className__")||""))})},hasClass:function(t){var e,n,i=0;for(e=" "+t+" ";n=this[i++];)if(1===n.nodeType&&(" "+ue(ce(n))+" ").indexOf(e)>-1)return!0;return!1}});var de=/\r/g;m.fn.extend({val:function(t){var e,n,i,r=this[0];return arguments.length?(i=m.isFunction(t),this.each(function(n){var r;1===this.nodeType&&(null==(r=i?t.call(this,n,m(this).val()):t)?r="":"number"==typeof r?r+="":Array.isArray(r)&&(r=m.map(r,function(t){return null==t?"":t+""})),(e=m.valHooks[this.type]||m.valHooks[this.nodeName.toLowerCase()])&&"set"in e&&void 0!==e.set(this,r,"value")||(this.value=r))})):r?(e=m.valHooks[r.type]||m.valHooks[r.nodeName.toLowerCase()])&&"get"in e&&void 0!==(n=e.get(r,"value"))?n:"string"==typeof(n=r.value)?n.replace(de,""):null==n?"":n:void 0}}),m.extend({valHooks:{option:{get:function(t){var e=m.find.attr(t,"value");return null!=e?e:ue(m.text(t))}},select:{get:function(t){var e,n,i,r=t.options,o=t.selectedIndex,a="select-one"===t.type,s=a?null:[],l=a?o+1:r.length;for(i=o<0?l:a?o:0;i-1)&&(n=!0);return n||(t.selectedIndex=-1),o}}}}),m.each(["radio","checkbox"],function(){m.valHooks[this]={set:function(t,e){if(Array.isArray(e))return t.checked=m.inArray(m(t).val(),e)>-1}},p.checkOn||(m.valHooks[this].get=function(t){return null===t.getAttribute("value")?"on":t.value})});var he=/^(?:focusinfocus|focusoutblur)$/;m.extend(m.event,{trigger:function(e,n,r,o){var a,s,l,u,c,h,f,p=[r||i],g=d.call(e,"type")?e.type:e,v=d.call(e,"namespace")?e.namespace.split("."):[];if(s=l=r=r||i,3!==r.nodeType&&8!==r.nodeType&&!he.test(g+m.event.triggered)&&(g.indexOf(".")>-1&&(g=(v=g.split(".")).shift(),v.sort()),c=g.indexOf(":")<0&&"on"+g,(e=e[m.expando]?e:new m.Event(g,"object"==typeof e&&e)).isTrigger=o?2:3,e.namespace=v.join("."),e.rnamespace=e.namespace?new RegExp("(^|\\.)"+v.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,e.result=void 0,e.target||(e.target=r),n=null==n?[e]:m.makeArray(n,[e]),f=m.event.special[g]||{},o||!f.trigger||!1!==f.trigger.apply(r,n))){if(!o&&!f.noBubble&&!m.isWindow(r)){for(u=f.delegateType||g,he.test(u+g)||(s=s.parentNode);s;s=s.parentNode)p.push(s),l=s;l===(r.ownerDocument||i)&&p.push(l.defaultView||l.parentWindow||t)}for(a=0;(s=p[a++])&&!e.isPropagationStopped();)e.type=a>1?u:f.bindType||g,(h=(V.get(s,"events")||{})[e.type]&&V.get(s,"handle"))&&h.apply(s,n),(h=c&&s[c])&&h.apply&&z(s)&&(e.result=h.apply(s,n),!1===e.result&&e.preventDefault());return e.type=g,o||e.isDefaultPrevented()||f._default&&!1!==f._default.apply(p.pop(),n)||!z(r)||c&&m.isFunction(r[g])&&!m.isWindow(r)&&((l=r[c])&&(r[c]=null),m.event.triggered=g,r[g](),m.event.triggered=void 0,l&&(r[c]=l)),e.result}},simulate:function(t,e,n){var i=m.extend(new m.Event,n,{type:t,isSimulated:!0});m.event.trigger(i,null,e)}}),m.fn.extend({trigger:function(t,e){return this.each(function(){m.event.trigger(t,e,this)})},triggerHandler:function(t,e){var n=this[0];if(n)return m.event.trigger(t,e,n,!0)}}),m.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(t,e){m.fn[e]=function(t,n){return arguments.length>0?this.on(e,null,t,n):this.trigger(e)}}),m.fn.extend({hover:function(t,e){return this.mouseenter(t).mouseleave(e||t)}}),p.focusin="onfocusin"in t,p.focusin||m.each({focus:"focusin",blur:"focusout"},function(t,e){var n=function(t){m.event.simulate(e,t.target,m.event.fix(t))};m.event.special[e]={setup:function(){var i=this.ownerDocument||this,r=V.access(i,e);r||i.addEventListener(t,n,!0),V.access(i,e,(r||0)+1)},teardown:function(){var i=this.ownerDocument||this,r=V.access(i,e)-1;r?V.access(i,e,r):(i.removeEventListener(t,n,!0),V.remove(i,e))}}});var fe=t.location,pe=m.now(),ge=/\?/;m.parseXML=function(e){var n;if(!e||"string"!=typeof e)return null;try{n=(new t.DOMParser).parseFromString(e,"text/xml")}catch(t){n=void 0}return n&&!n.getElementsByTagName("parsererror").length||m.error("Invalid XML: "+e),n};var me=/\[\]$/,ve=/\r?\n/g,ye=/^(?:submit|button|image|reset|file)$/i,be=/^(?:input|select|textarea|keygen)/i;function _e(t,e,n,i){var r;if(Array.isArray(e))m.each(e,function(e,r){n||me.test(t)?i(t,r):_e(t+"["+("object"==typeof r&&null!=r?e:"")+"]",r,n,i)});else if(n||"object"!==m.type(e))i(t,e);else for(r in e)_e(t+"["+r+"]",e[r],n,i)}m.param=function(t,e){var n,i=[],r=function(t,e){var n=m.isFunction(e)?e():e;i[i.length]=encodeURIComponent(t)+"="+encodeURIComponent(null==n?"":n)};if(Array.isArray(t)||t.jquery&&!m.isPlainObject(t))m.each(t,function(){r(this.name,this.value)});else for(n in t)_e(n,t[n],e,r);return i.join("&")},m.fn.extend({serialize:function(){return m.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var t=m.prop(this,"elements");return t?m.makeArray(t):this}).filter(function(){var t=this.type;return this.name&&!m(this).is(":disabled")&&be.test(this.nodeName)&&!ye.test(t)&&(this.checked||!ot.test(t))}).map(function(t,e){var n=m(this).val();return null==n?null:Array.isArray(n)?m.map(n,function(t){return{name:e.name,value:t.replace(ve,"\r\n")}}):{name:e.name,value:n.replace(ve,"\r\n")}}).get()}});var xe=/%20/g,we=/#.*$/,Ce=/([?&])_=[^&]*/,ke=/^(.*?):[ \t]*([^\r\n]*)$/gm,Se=/^(?:GET|HEAD)$/,Te=/^\/\//,De={},Oe={},Ae="*/".concat("*"),Me=i.createElement("a");function Ee(t){return function(e,n){"string"!=typeof e&&(n=e,e="*");var i,r=0,o=e.toLowerCase().match(F)||[];if(m.isFunction(n))for(;i=o[r++];)"+"===i[0]?(i=i.slice(1)||"*",(t[i]=t[i]||[]).unshift(n)):(t[i]=t[i]||[]).push(n)}}function Ie(t,e,n,i){var r={},o=t===Oe;function a(s){var l;return r[s]=!0,m.each(t[s]||[],function(t,s){var u=s(e,n,i);return"string"!=typeof u||o||r[u]?o?!(l=u):void 0:(e.dataTypes.unshift(u),a(u),!1)}),l}return a(e.dataTypes[0])||!r["*"]&&a("*")}function Pe(t,e){var n,i,r=m.ajaxSettings.flatOptions||{};for(n in e)void 0!==e[n]&&((r[n]?t:i||(i={}))[n]=e[n]);return i&&m.extend(!0,t,i),t}Me.href=fe.href,m.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:fe.href,type:"GET",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(fe.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Ae,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":m.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(t,e){return e?Pe(Pe(t,m.ajaxSettings),e):Pe(m.ajaxSettings,t)},ajaxPrefilter:Ee(De),ajaxTransport:Ee(Oe),ajax:function(e,n){"object"==typeof e&&(n=e,e=void 0),n=n||{};var r,o,a,s,l,u,c,d,h,f,p=m.ajaxSetup({},n),g=p.context||p,v=p.context&&(g.nodeType||g.jquery)?m(g):m.event,y=m.Deferred(),b=m.Callbacks("once memory"),_=p.statusCode||{},x={},w={},C="canceled",k={readyState:0,getResponseHeader:function(t){var e;if(c){if(!s)for(s={};e=ke.exec(a);)s[e[1].toLowerCase()]=e[2];e=s[t.toLowerCase()]}return null==e?null:e},getAllResponseHeaders:function(){return c?a:null},setRequestHeader:function(t,e){return null==c&&(t=w[t.toLowerCase()]=w[t.toLowerCase()]||t,x[t]=e),this},overrideMimeType:function(t){return null==c&&(p.mimeType=t),this},statusCode:function(t){var e;if(t)if(c)k.always(t[k.status]);else for(e in t)_[e]=[_[e],t[e]];return this},abort:function(t){var e=t||C;return r&&r.abort(e),S(0,e),this}};if(y.promise(k),p.url=((e||p.url||fe.href)+"").replace(Te,fe.protocol+"//"),p.type=n.method||n.type||p.method||p.type,p.dataTypes=(p.dataType||"*").toLowerCase().match(F)||[""],null==p.crossDomain){u=i.createElement("a");try{u.href=p.url,u.href=u.href,p.crossDomain=Me.protocol+"//"+Me.host!=u.protocol+"//"+u.host}catch(t){p.crossDomain=!0}}if(p.data&&p.processData&&"string"!=typeof p.data&&(p.data=m.param(p.data,p.traditional)),Ie(De,p,n,k),c)return k;for(h in(d=m.event&&p.global)&&0==m.active++&&m.event.trigger("ajaxStart"),p.type=p.type.toUpperCase(),p.hasContent=!Se.test(p.type),o=p.url.replace(we,""),p.hasContent?p.data&&p.processData&&0===(p.contentType||"").indexOf("application/x-www-form-urlencoded")&&(p.data=p.data.replace(xe,"+")):(f=p.url.slice(o.length),p.data&&(o+=(ge.test(o)?"&":"?")+p.data,delete p.data),!1===p.cache&&(o=o.replace(Ce,"$1"),f=(ge.test(o)?"&":"?")+"_="+pe+++f),p.url=o+f),p.ifModified&&(m.lastModified[o]&&k.setRequestHeader("If-Modified-Since",m.lastModified[o]),m.etag[o]&&k.setRequestHeader("If-None-Match",m.etag[o])),(p.data&&p.hasContent&&!1!==p.contentType||n.contentType)&&k.setRequestHeader("Content-Type",p.contentType),k.setRequestHeader("Accept",p.dataTypes[0]&&p.accepts[p.dataTypes[0]]?p.accepts[p.dataTypes[0]]+("*"!==p.dataTypes[0]?", "+Ae+"; q=0.01":""):p.accepts["*"]),p.headers)k.setRequestHeader(h,p.headers[h]);if(p.beforeSend&&(!1===p.beforeSend.call(g,k,p)||c))return k.abort();if(C="abort",b.add(p.complete),k.done(p.success),k.fail(p.error),r=Ie(Oe,p,n,k)){if(k.readyState=1,d&&v.trigger("ajaxSend",[k,p]),c)return k;p.async&&p.timeout>0&&(l=t.setTimeout(function(){k.abort("timeout")},p.timeout));try{c=!1,r.send(x,S)}catch(t){if(c)throw t;S(-1,t)}}else S(-1,"No Transport");function S(e,n,i,s){var u,h,f,x,w,C=n;c||(c=!0,l&&t.clearTimeout(l),r=void 0,a=s||"",k.readyState=e>0?4:0,u=e>=200&&e<300||304===e,i&&(x=function(t,e,n){for(var i,r,o,a,s=t.contents,l=t.dataTypes;"*"===l[0];)l.shift(),void 0===i&&(i=t.mimeType||e.getResponseHeader("Content-Type"));if(i)for(r in s)if(s[r]&&s[r].test(i)){l.unshift(r);break}if(l[0]in n)o=l[0];else{for(r in n){if(!l[0]||t.converters[r+" "+l[0]]){o=r;break}a||(a=r)}o=o||a}if(o)return o!==l[0]&&l.unshift(o),n[o]}(p,k,i)),x=function(t,e,n,i){var r,o,a,s,l,u={},c=t.dataTypes.slice();if(c[1])for(a in t.converters)u[a.toLowerCase()]=t.converters[a];for(o=c.shift();o;)if(t.responseFields[o]&&(n[t.responseFields[o]]=e),!l&&i&&t.dataFilter&&(e=t.dataFilter(e,t.dataType)),l=o,o=c.shift())if("*"===o)o=l;else if("*"!==l&&l!==o){if(!(a=u[l+" "+o]||u["* "+o]))for(r in u)if((s=r.split(" "))[1]===o&&(a=u[l+" "+s[0]]||u["* "+s[0]])){!0===a?a=u[r]:!0!==u[r]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&t.throws)e=a(e);else try{e=a(e)}catch(t){return{state:"parsererror",error:a?t:"No conversion from "+l+" to "+o}}}return{state:"success",data:e}}(p,x,k,u),u?(p.ifModified&&((w=k.getResponseHeader("Last-Modified"))&&(m.lastModified[o]=w),(w=k.getResponseHeader("etag"))&&(m.etag[o]=w)),204===e||"HEAD"===p.type?C="nocontent":304===e?C="notmodified":(C=x.state,h=x.data,u=!(f=x.error))):(f=C,!e&&C||(C="error",e<0&&(e=0))),k.status=e,k.statusText=(n||C)+"",u?y.resolveWith(g,[h,C,k]):y.rejectWith(g,[k,C,f]),k.statusCode(_),_=void 0,d&&v.trigger(u?"ajaxSuccess":"ajaxError",[k,p,u?h:f]),b.fireWith(g,[k,C]),d&&(v.trigger("ajaxComplete",[k,p]),--m.active||m.event.trigger("ajaxStop")))}return k},getJSON:function(t,e,n){return m.get(t,e,n,"json")},getScript:function(t,e){return m.get(t,void 0,e,"script")}}),m.each(["get","post"],function(t,e){m[e]=function(t,n,i,r){return m.isFunction(n)&&(r=r||i,i=n,n=void 0),m.ajax(m.extend({url:t,type:e,dataType:r,data:n,success:i},m.isPlainObject(t)&&t))}}),m._evalUrl=function(t){return m.ajax({url:t,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,throws:!0})},m.fn.extend({wrapAll:function(t){var e;return this[0]&&(m.isFunction(t)&&(t=t.call(this[0])),e=m(t,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&e.insertBefore(this[0]),e.map(function(){for(var t=this;t.firstElementChild;)t=t.firstElementChild;return t}).append(this)),this},wrapInner:function(t){return m.isFunction(t)?this.each(function(e){m(this).wrapInner(t.call(this,e))}):this.each(function(){var e=m(this),n=e.contents();n.length?n.wrapAll(t):e.append(t)})},wrap:function(t){var e=m.isFunction(t);return this.each(function(n){m(this).wrapAll(e?t.call(this,n):t)})},unwrap:function(t){return this.parent(t).not("body").each(function(){m(this).replaceWith(this.childNodes)}),this}}),m.expr.pseudos.hidden=function(t){return!m.expr.pseudos.visible(t)},m.expr.pseudos.visible=function(t){return!!(t.offsetWidth||t.offsetHeight||t.getClientRects().length)},m.ajaxSettings.xhr=function(){try{return new t.XMLHttpRequest}catch(t){}};var Ne={0:200,1223:204},Fe=m.ajaxSettings.xhr();p.cors=!!Fe&&"withCredentials"in Fe,p.ajax=Fe=!!Fe,m.ajaxTransport(function(e){var n,i;if(p.cors||Fe&&!e.crossDomain)return{send:function(r,o){var a,s=e.xhr();if(s.open(e.type,e.url,e.async,e.username,e.password),e.xhrFields)for(a in e.xhrFields)s[a]=e.xhrFields[a];for(a in e.mimeType&&s.overrideMimeType&&s.overrideMimeType(e.mimeType),e.crossDomain||r["X-Requested-With"]||(r["X-Requested-With"]="XMLHttpRequest"),r)s.setRequestHeader(a,r[a]);n=function(t){return function(){n&&(n=i=s.onload=s.onerror=s.onabort=s.onreadystatechange=null,"abort"===t?s.abort():"error"===t?"number"!=typeof s.status?o(0,"error"):o(s.status,s.statusText):o(Ne[s.status]||s.status,s.statusText,"text"!==(s.responseType||"text")||"string"!=typeof s.responseText?{binary:s.response}:{text:s.responseText},s.getAllResponseHeaders()))}},s.onload=n(),i=s.onerror=n("error"),void 0!==s.onabort?s.onabort=i:s.onreadystatechange=function(){4===s.readyState&&t.setTimeout(function(){n&&i()})},n=n("abort");try{s.send(e.hasContent&&e.data||null)}catch(t){if(n)throw t}},abort:function(){n&&n()}}}),m.ajaxPrefilter(function(t){t.crossDomain&&(t.contents.script=!1)}),m.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(t){return m.globalEval(t),t}}}),m.ajaxPrefilter("script",function(t){void 0===t.cache&&(t.cache=!1),t.crossDomain&&(t.type="GET")}),m.ajaxTransport("script",function(t){var e,n;if(t.crossDomain)return{send:function(r,o){e=m(" +@endpush \ No newline at end of file diff --git a/resources/views/themes/base/admin/approve_comment.blade.php b/resources/views/themes/base/admin/approve_comment.blade.php new file mode 100644 index 0000000..415b2d8 --- /dev/null +++ b/resources/views/themes/base/admin/approve_comment.blade.php @@ -0,0 +1,34 @@ +@extends(Theme::viewName('layout')) +@section('title', trans('admin.approve_comment', ['author_name' => $comment->authorDisplayName()])) + +@section('breadcrumb') + + + + +@endsection + +@section('content') +
+
+
+
+
@yield('title')
+
+

@lang('admin.approve_comment_confirm', ['author_name' => $comment->authorDisplayName()])

+ + {!! $comment->textAsHtml() !!} + +
+
+ {{ csrf_field() }} + @lang('forms.cancel_action') + +
+
+
+
+
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/themes/base/admin/bulk_approve_comments.blade.php b/resources/views/themes/base/admin/bulk_approve_comments.blade.php new file mode 100644 index 0000000..c2a8c39 --- /dev/null +++ b/resources/views/themes/base/admin/bulk_approve_comments.blade.php @@ -0,0 +1,37 @@ +@extends(Theme::viewName('layout')) +@section('title', trans('admin.approve_comments', ['number' => $comment_count])) + +@section('breadcrumb') + + + + +@endsection + +@section('content') +
+
+
+
+
@yield('title')
+
+

@lang('admin.approve_comments_confirm', ['number' => $comment_count])

+ +
+
+ {{ csrf_field() }} + + @foreach ($comment_ids as $comment_id) + + @endforeach + + @lang('forms.cancel_action') + +
+
+
+
+
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/themes/base/admin/bulk_delete_comments.blade.php b/resources/views/themes/base/admin/bulk_delete_comments.blade.php new file mode 100644 index 0000000..c68854c --- /dev/null +++ b/resources/views/themes/base/admin/bulk_delete_comments.blade.php @@ -0,0 +1,39 @@ +@extends(Theme::viewName('layout')) +@section('title', trans('admin.delete_comments', ['number' => $comment_count])) + +@section('breadcrumb') + + + + +@endsection + +@section('content') +
+
+
+
+
@yield('title')
+
+

@lang('admin.delete_comments_confirm', ['number' => $comment_count])

+ +

@lang('admin.delete_comments_warning')

+ +
+
+ {{ csrf_field() }} + + @foreach ($comment_ids as $comment_id) + + @endforeach + + @lang('forms.cancel_action') + +
+
+
+
+
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/themes/base/admin/bulk_reject_comments.blade.php b/resources/views/themes/base/admin/bulk_reject_comments.blade.php new file mode 100644 index 0000000..d753a52 --- /dev/null +++ b/resources/views/themes/base/admin/bulk_reject_comments.blade.php @@ -0,0 +1,37 @@ +@extends(Theme::viewName('layout')) +@section('title', trans('admin.reject_comments', ['number' => $comment_count])) + +@section('breadcrumb') + + + + +@endsection + +@section('content') +
+
+
+
+
@yield('title')
+
+

@lang('admin.reject_comments_confirm', ['number' => $comment_count])

+ +
+
+ {{ csrf_field() }} + + @foreach ($comment_ids as $comment_id) + + @endforeach + + @lang('forms.cancel_action') + +
+
+
+
+
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/themes/base/admin/create_album.blade.php b/resources/views/themes/base/admin/create_album.blade.php index db9e9c1..0f41cce 100644 --- a/resources/views/themes/base/admin/create_album.blade.php +++ b/resources/views/themes/base/admin/create_album.blade.php @@ -54,7 +54,7 @@ -
+
-
+
is_permissions_inherited ? ' checked="checked"' : '' }}>
diff --git a/resources/views/themes/base/email/moderate_photo_comment.blade.php b/resources/views/themes/base/email/moderate_photo_comment.blade.php new file mode 100644 index 0000000..5c7d8ef --- /dev/null +++ b/resources/views/themes/base/email/moderate_photo_comment.blade.php @@ -0,0 +1,26 @@ +@component('mail::message') +@lang('email.generic_intro', ['user_name' => $user->name]) + + +@lang('email.moderate_photo_comment_p1', ['album_name' => $album->name]) + + +@lang('email.moderate_photo_comment_p2') + + +@lang('email.moderate_photo_comment_name_label') {{ $comment->name }} + +@lang('email.moderate_photo_comment_email_label') {{ $comment->email }} + +@lang('email.moderate_photo_comment_comment_label') + +{!! $comment->comment !!} + +@component('mail::button', ['url' => $photo->url(), 'color' => 'blue']) + @lang('forms.review_photo_comment_action') +@endcomponent + +@lang('email.generic_regards')
+{{ UserConfig::get('app_name') }}
+{{ route('home') }} +@endcomponent \ No newline at end of file diff --git a/resources/views/themes/base/email/photo_comment_approved.blade.php b/resources/views/themes/base/email/photo_comment_approved.blade.php new file mode 100644 index 0000000..2e64972 --- /dev/null +++ b/resources/views/themes/base/email/photo_comment_approved.blade.php @@ -0,0 +1,26 @@ +@component('mail::message') +@lang('email.generic_intro', ['user_name' => $user->name]) + + +@lang('email.photo_comment_approved_p1', ['album_name' => $album->name]) + + +@lang('email.photo_comment_approved_p2') + + +@lang('email.photo_comment_approved_name_label') {{ $comment->name }} + +@lang('email.photo_comment_approved_email_label') {{ $comment->email }} + +@lang('email.photo_comment_approved_comment_label') + +{!! $comment->comment !!} + +@component('mail::button', ['url' => $photo->url(), 'color' => 'blue']) + @lang('forms.view_photo_comment_action') +@endcomponent + +@lang('email.generic_regards')
+{{ UserConfig::get('app_name') }}
+{{ route('home') }} +@endcomponent \ No newline at end of file diff --git a/resources/views/themes/base/email/photo_comment_approved_user.blade.php b/resources/views/themes/base/email/photo_comment_approved_user.blade.php new file mode 100644 index 0000000..00c930a --- /dev/null +++ b/resources/views/themes/base/email/photo_comment_approved_user.blade.php @@ -0,0 +1,22 @@ +@component('mail::message') +@lang('email.generic_intro', ['user_name' => $user->name]) + + +@lang('email.photo_comment_approved_user_p1', ['album_name' => $album->name]) + + +@lang('email.photo_comment_approved_user_p2') + + +@lang('email.photo_comment_approved_user_comment_label') + +{!! $comment->comment !!} + +@component('mail::button', ['url' => $photo->url(), 'color' => 'blue']) + @lang('forms.view_photo_comment_action') +@endcomponent + +@lang('email.generic_regards')
+{{ UserConfig::get('app_name') }}
+{{ route('home') }} +@endcomponent \ No newline at end of file diff --git a/resources/views/themes/base/email/photo_comment_replied_to.blade.php b/resources/views/themes/base/email/photo_comment_replied_to.blade.php new file mode 100644 index 0000000..49eacf6 --- /dev/null +++ b/resources/views/themes/base/email/photo_comment_replied_to.blade.php @@ -0,0 +1,22 @@ +@component('mail::message') +@lang('email.generic_intro', ['user_name' => $user->name]) + + +@lang('email.photo_comment_replied_to_p1', ['album_name' => $album->name]) + + +@lang('email.photo_comment_replied_to_p2') + + +@lang('email.photo_comment_replied_to_comment_label') + +{!! $comment->comment !!} + +@component('mail::button', ['url' => $photo->url(), 'color' => 'blue']) + @lang('forms.view_photo_comment_action') +@endcomponent + +@lang('email.generic_regards')
+{{ UserConfig::get('app_name') }}
+{{ route('home') }} +@endcomponent \ No newline at end of file diff --git a/resources/views/themes/base/gallery/photo.blade.php b/resources/views/themes/base/gallery/photo.blade.php index e380356..4119eb8 100644 --- a/resources/views/themes/base/gallery/photo.blade.php +++ b/resources/views/themes/base/gallery/photo.blade.php @@ -10,7 +10,7 @@ @endsection @section('content') -
+

{{ $photo->name }}

@@ -21,7 +21,7 @@
- -
+
Information about this photo:
@@ -153,4 +157,16 @@
-@endsection \ No newline at end of file +@endsection + +@push('scripts') + +@endpush \ No newline at end of file diff --git a/resources/views/themes/base/partials/admin_manage_widget.blade.php b/resources/views/themes/base/partials/admin_manage_widget.blade.php index 082dcf2..a5db0f9 100644 --- a/resources/views/themes/base/partials/admin_manage_widget.blade.php +++ b/resources/views/themes/base/partials/admin_manage_widget.blade.php @@ -5,6 +5,7 @@ $canManageLabels = Auth::user()->can('admin:manage-labels'); $canManageStorage = Auth::user()->can('admin:manage-storage'); $canManageUsers = Auth::user()->can('admin:manage-users'); + $canManageComments = Auth::user()->can('admin:manage-comments'); @endphp @if ($canConfigure || $canManageAlbums || $canManageGroups || $canManageStorage || $canManageUsers) @@ -17,6 +18,9 @@ @if ($canManageLabels)
@lang('navigation.breadcrumb.labels') @endif + @if ($canManageComments) + @lang('navigation.breadcrumb.comments') + @endif @if ($canManageUsers) @lang('navigation.breadcrumb.users') @endif diff --git a/resources/views/themes/base/partials/admin_stats_widget.blade.php b/resources/views/themes/base/partials/admin_stats_widget.blade.php index 3f45961..e65c6eb 100644 --- a/resources/views/themes/base/partials/admin_stats_widget.blade.php +++ b/resources/views/themes/base/partials/admin_stats_widget.blade.php @@ -2,7 +2,8 @@
@lang('admin.stats_widget.panel_header')
{{ $album_count }} {{ trans_choice('admin.stats_widget.albums', $album_count) }} · - {{ $photo_count }} {{ trans_choice('admin.stats_widget.photos', $photo_count) }}
+ {{ $photo_count }} {{ trans_choice('admin.stats_widget.photos', $photo_count) }} · + {{ $comment_count }} {{ trans_choice('admin.stats_widget.comments', $comment_count) }}
{{ $label_count }} {{ trans_choice('admin.stats_widget.labels', $label_count) }} @can('admin:access')
diff --git a/resources/views/themes/base/partials/album_permissions.blade.php b/resources/views/themes/base/partials/album_permissions.blade.php index 6684d9a..c7dd426 100644 --- a/resources/views/themes/base/partials/album_permissions.blade.php +++ b/resources/views/themes/base/partials/album_permissions.blade.php @@ -14,8 +14,10 @@ @include(Theme::viewName('partials.permission_checkbox'), ['permission' => Theme::getPermission($all_permissions, 'album', 'list')]) @include(Theme::viewName('partials.permission_checkbox'), ['permission' => Theme::getPermission($all_permissions, 'album', 'view')]) + @include(Theme::viewName('partials.permission_checkbox'), ['permission' => Theme::getPermission($all_permissions, 'album', 'post-comment')]) @if ($object_id != 'anonymous') + @include(Theme::viewName('partials.permission_checkbox'), ['permission' => Theme::getPermission($all_permissions, 'album', 'moderate-comments')]) @include(Theme::viewName('partials.permission_checkbox'), ['permission' => Theme::getPermission($all_permissions, 'album', 'edit')]) @include(Theme::viewName('partials.permission_checkbox'), ['permission' => Theme::getPermission($all_permissions, 'album', 'delete')]) @endif diff --git a/resources/views/themes/base/partials/album_permissions_tab.blade.php b/resources/views/themes/base/partials/album_permissions_tab.blade.php index 249a180..0c37c12 100644 --- a/resources/views/themes/base/partials/album_permissions_tab.blade.php +++ b/resources/views/themes/base/partials/album_permissions_tab.blade.php @@ -1,15 +1,31 @@
@if ($album->is_permissions_inherited) -
-

@lang('admin.album_inheriting_permissions_p1')

-

@lang('admin.album_inheriting_permissions_p2')

-

@lang('admin.album_inheriting_permissions_p3', [ - 'l_parent_start' => sprintf('', route('albums.show', [$album->effectiveAlbumIDForPermissions(), 'tab' => 'permissions'])), - 'l_parent_end' => '', - 'l_edit_start' => sprintf('', route('albums.edit', [$album->id])), - 'l_edit_end' => '' - ])

-
+ @php + $effectiveAlbumID = $album->effectiveAlbumIDForPermissions() + @endphp + @if ($effectiveAlbumID > 0) +
+

@lang('admin.album_inheriting_permissions_p1')

+

@lang('admin.album_inheriting_permissions_p2')

+

@lang('admin.album_inheriting_permissions_p3', [ + 'l_parent_start' => sprintf('', route('albums.show', [$album->effectiveAlbumIDForPermissions(), 'tab' => 'permissions'])), + 'l_parent_end' => '', + 'l_edit_start' => sprintf('', route('albums.edit', [$album->id])), + 'l_edit_end' => '' + ])

+
+ @else +
+

@lang('admin.album_inheriting_permissions_p1')

+

@lang('admin.album_inheriting_permissions_p2_toplevel')

+

@lang('admin.album_inheriting_permissions_p3_toplevel', [ + 'l_defperms_start' => sprintf('', route('albums.defaultPermissions')), + 'l_defperms_end' => '', + 'l_edit_start' => sprintf('', route('albums.edit', [$album->id])), + 'l_edit_end' => '' + ])

+
+ @endif @else

@lang('admin.security_heading')

@lang('admin.security_text')

diff --git a/resources/views/themes/base/partials/photo_comments.blade.php b/resources/views/themes/base/partials/photo_comments.blade.php new file mode 100644 index 0000000..a37d8a9 --- /dev/null +++ b/resources/views/themes/base/partials/photo_comments.blade.php @@ -0,0 +1,42 @@ +
+
+

@lang('gallery.photo_comments_heading')

+ + @if (\App\User::currentOrAnonymous()->can('post-comment', $photo)) +

@lang('gallery.photo_comments_reply_form_heading')

+

@lang('gallery.photo_comments_reply_form_p1')

+
+ @include(Theme::viewName('partials.photo_comments_reply_form')) + @endif + + @foreach ($photo->comments()->whereNull('parent_comment_id')->get() as $comment) + @if ($comment->isApproved()) + @include(Theme::viewName('partials.photo_single_comment')) + @elseif (!$comment->isModerated() && Gate::allows('moderate-comments', $photo)) + @include(Theme::viewName('partials.photo_single_comment_moderate')) + @endif + @endforeach +
+
+ + \ No newline at end of file diff --git a/resources/views/themes/base/partials/photo_comments_reply_form.blade.php b/resources/views/themes/base/partials/photo_comments_reply_form.blade.php new file mode 100644 index 0000000..ac7e524 --- /dev/null +++ b/resources/views/themes/base/partials/photo_comments_reply_form.blade.php @@ -0,0 +1,82 @@ +@php +$is_reply = isset($reply_comment); +$is_known_user = !is_null(Auth::user()) +@endphp + +{{-- Show a preview of the comment we're replying to --}} +@if ($is_reply) +
+ @include (Theme::viewName('partials.photo_single_comment'), ['comment' => $reply_comment, 'is_reply' => true]) +
+@endif + +
+ {{ csrf_field() }} + + @if ($is_reply) + + @endif + +
+ + + + @if ($errors->has('name')) +
+ {{ $errors->first('name') }} +
+ @endif +
+ +
+ + + @lang('forms.photo_comment_email_help') + + @if ($errors->has('email')) +
+ {{ $errors->first('email') }} +
+ @endif + + @if ($is_known_user) + @lang('gallery.not_you_logout') + @endif +
+ +
+ + + + @if ($errors->has('comment')) +
+ {{ $errors->first('comment') }} +
+ @endif +
+ +
+ @if ($is_reply) + + @else + + @endif +
+
+ +@push('scripts') + + +@endpush \ No newline at end of file diff --git a/resources/views/themes/base/partials/photo_single_comment.blade.php b/resources/views/themes/base/partials/photo_single_comment.blade.php new file mode 100644 index 0000000..349f0e8 --- /dev/null +++ b/resources/views/themes/base/partials/photo_single_comment.blade.php @@ -0,0 +1,26 @@ +@php + $is_reply = (isset($is_reply) && $is_reply); +@endphp + +
+
+ {{ $comment->authorDisplayName() }} +
{{ $comment->authorDisplayName() }}
+
{{ date(UserConfig::get('date_format'), strtotime($comment->created_at)) }}
+ {!! $comment->textAsHtml() !!} + + @if (!$is_reply && ($comment->depth() < UserConfig::get('photo_comments_thread_depth') - 1) && \App\User::currentOrAnonymous()->can('post-comment', $photo)) + @lang('gallery.photo_comments_reply_action') + @endif +
+
+ +@if (!$is_reply) + @foreach ($comment->children as $childComment) + @if ($childComment->isApproved()) + @include(Theme::viewName('partials.photo_single_comment'), ['comment' => $childComment]) + @elseif (!$childComment->isModerated() && Gate::allows('moderate-comments', $photo)) + @include(Theme::viewName('partials.photo_single_comment_moderate'), ['comment' => $childComment]) + @endif + @endforeach +@endif \ No newline at end of file diff --git a/resources/views/themes/base/partials/photo_single_comment_moderate.blade.php b/resources/views/themes/base/partials/photo_single_comment_moderate.blade.php new file mode 100644 index 0000000..c8a9d8a --- /dev/null +++ b/resources/views/themes/base/partials/photo_single_comment_moderate.blade.php @@ -0,0 +1,17 @@ +
+
+ @lang('gallery.photo_comment_pending_approval') +
+
+ {{ $comment->authorDisplayName() }} +
{{ $comment->authorDisplayName() }}
+
{{ date(UserConfig::get('date_format'), strtotime($comment->created_at)) }}
+ {!! $comment->textAsHtml() !!} + +
+ {{ csrf_field() }} + + +
+
+
\ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 302b66a..abb18de 100644 --- a/routes/web.php +++ b/routes/web.php @@ -27,6 +27,9 @@ Route::group(['prefix' => 'admin'], function () { Route::post('statistics/save', 'Admin\StatisticsController@save')->name('admin.statistics.save'); // Album management + Route::get('albums/default-permissions', 'Admin\AlbumController@defaultPermissions')->name('albums.defaultPermissions'); + Route::post('albums/set-default-group-permissions', 'Admin\AlbumController@setDefaultGroupPermissions')->name('albums.set_default_group_permissions'); + Route::post('albums/set-default-user-permissions', 'Admin\AlbumController@setDefaultUserPermissions')->name('albums.set_default_user_permissions'); Route::get('albums/{id}/analyse/{queue_token}', 'Admin\AlbumController@analyse')->name('albums.analyse'); Route::get('albums/{id}/delete', 'Admin\AlbumController@delete')->name('albums.delete'); Route::get('/albums/{id}/metadata', 'Admin\AlbumController@metadata')->name('albums.metadata'); @@ -63,6 +66,19 @@ Route::group(['prefix' => 'admin'], function () { // Group management Route::get('groups/{id}/delete', 'Admin\GroupController@delete')->name('groups.delete'); Route::resource('groups', 'Admin\GroupController'); + + // Comments management + Route::get('comments/{id}/approve', 'Admin\PhotoCommentController@approve')->name('comments.approve'); + Route::post('comments/{id}/approve', 'Admin\PhotoCommentController@confirmApprove')->name('comments.confirmApprove'); + + Route::get('comments/{id}/reject', 'Admin\PhotoCommentController@reject')->name('comments.reject'); + Route::post('comments/{id}/reject', 'Admin\PhotoCommentController@confirmReject')->name('comments.confirmReject'); + + Route::get('comments/{id}/delete', 'Admin\PhotoCommentController@delete')->name('comments.delete'); + + Route::post('comments/apply-bulk-action', 'Admin\PhotoCommentController@applyBulkAction')->name('comments.applyBulkAction'); + Route::post('comments/bulk-action', 'Admin\PhotoCommentController@bulkAction')->name('comments.bulkAction'); + Route::resource('comments', 'Admin\PhotoCommentController'); }); // Installation @@ -106,6 +122,15 @@ Route::get('a/{albumUrlAlias}', 'Gallery\AlbumController@index') Route::get('exif/{albumUrlAlias}/{photoFilename}', 'Gallery\PhotoController@showExifData') ->name('viewExifData') ->where('albumUrlAlias', '.*'); +Route::post('p/{albumUrlAlias}/{photoFilename}/comments', 'Gallery\PhotoCommentController@store') + ->name('postPhotoComment') + ->where('albumUrlAlias', '.*'); +Route::post('p/{albumUrlAlias}/{photoFilename}/comments/moderate/{commentID}', 'Gallery\PhotoCommentController@moderate') + ->name('moderatePhotoComment') + ->where('albumUrlAlias', '.*'); +Route::get('p/{albumUrlAlias}/{photoFilename}/comments/reply/{commentID}', 'Gallery\PhotoCommentController@reply') + ->name('replyPhotoComment') + ->where('albumUrlAlias', '.*'); Route::get('p/{albumUrlAlias}/{photoFilename}', 'Gallery\PhotoController@show') ->name('viewPhoto') ->where('albumUrlAlias', '.*');