├── .editorconfig ├── .env.example ├── .env.testing ├── .gitattributes ├── .gitignore ├── .styleci.yml ├── Dockerfile ├── app ├── Album.php ├── Chest.php ├── Comment.php ├── Concerns │ └── Models │ │ ├── HasTags.php │ │ └── Postable.php ├── Console │ ├── Commands │ │ ├── CleanFiles.php │ │ ├── DecryptChests.php │ │ ├── EncryptChests.php │ │ ├── Install.php │ │ ├── LinkHealthChecks.php │ │ ├── ResetForDemo.php │ │ └── Update.php │ └── Kernel.php ├── Events │ ├── LinkArchiveRequested.php │ └── LinkHealthCheck.php ├── Exceptions │ └── Handler.php ├── Exports │ ├── ChestsExport.php │ ├── LinksExport.php │ └── StoriesExport.php ├── Http │ ├── Controllers │ │ ├── AccountController.php │ │ ├── AlbumController.php │ │ ├── Api │ │ │ ├── AccountController.php │ │ │ ├── AlbumController.php │ │ │ ├── ChestController.php │ │ │ ├── CommentController.php │ │ │ ├── LinkArchiveController.php │ │ │ ├── LinkController.php │ │ │ ├── Manage │ │ │ │ ├── ArchivesController.php │ │ │ │ ├── FeaturesController.php │ │ │ │ ├── LinksHealthController.php │ │ │ │ ├── TagsController.php │ │ │ │ ├── UsersController.php │ │ │ │ └── WallsController.php │ │ │ ├── SearchController.php │ │ │ ├── ShareController.php │ │ │ ├── StoryController.php │ │ │ └── TagsController.php │ │ ├── BrowseController.php │ │ ├── ChestController.php │ │ ├── Controller.php │ │ ├── FeedController.php │ │ ├── LinkArchiveController.php │ │ ├── LinkController.php │ │ ├── LoginController.php │ │ ├── Manage │ │ │ ├── ArchivesController.php │ │ │ ├── ExportController.php │ │ │ ├── LinksHealthController.php │ │ │ ├── SettingsController.php │ │ │ ├── TagsController.php │ │ │ ├── UsersController.php │ │ │ └── WallsController.php │ │ ├── PwaController.php │ │ ├── SecureLoginController.php │ │ ├── ShareController.php │ │ ├── StaticController.php │ │ └── StoryController.php │ ├── Kernel.php │ ├── Middleware │ │ ├── Authenticate.php │ │ ├── BlockInDemoMode.php │ │ ├── CheckForGlobalPrivacy.php │ │ ├── CheckForMaintenanceMode.php │ │ ├── EncryptCookies.php │ │ ├── ManageAccess.php │ │ ├── RedirectIfAuthenticated.php │ │ ├── TrimStrings.php │ │ ├── TrustProxies.php │ │ └── VerifyCsrfToken.php │ ├── Requests │ │ ├── AddCommentRequest.php │ │ ├── Manage │ │ │ ├── StoreUserRequest.php │ │ │ └── UpdateUserRequest.php │ │ ├── StoreAlbumRequest.php │ │ ├── StoreAlbumUploadRequest.php │ │ ├── StoreChestRequest.php │ │ ├── StoreLinkRequest.php │ │ ├── StoreSettingsRequest.php │ │ ├── StoreStoryRequest.php │ │ ├── StoreWallRequest.php │ │ ├── UpdateUserAccount.php │ │ └── UpdateUserPassword.php │ └── Resources │ │ ├── AlbumResource.php │ │ ├── ChestResource.php │ │ ├── CommentResource.php │ │ ├── LinkArchiveResource.php │ │ ├── LinkResource.php │ │ ├── PostResource.php │ │ ├── ShareResource.php │ │ ├── StoryResource.php │ │ ├── TagResource.php │ │ └── UserResource.php ├── Link.php ├── Listeners │ ├── CheckLinkHealth.php │ ├── MakeLinkArchive.php │ └── UpdateDatabase.php ├── Login.php ├── Notifications │ ├── CheckEmail.php │ ├── NewComment.php │ ├── NewUnmoderatedComment.php │ └── SecureLoginCode.php ├── Observers │ ├── CommentObserver.php │ ├── TagObserver.php │ └── WallObserver.php ├── Post.php ├── Providers │ ├── AppServiceProvider.php │ ├── AuthServiceProvider.php │ ├── BroadcastServiceProvider.php │ ├── EventServiceProvider.php │ └── RouteServiceProvider.php ├── SecureLogin.php ├── Services │ ├── Hashid.php │ ├── LinkArchive │ │ ├── BaseProvider.php │ │ ├── LinkArchive.php │ │ ├── PuppeteerProvider.php │ │ └── YoutubeDlProvider.php │ ├── LinkPreview │ │ ├── BaseProvider.php │ │ ├── LinkPreview.php │ │ ├── ProviderImage.php │ │ ├── ProviderImgur.php │ │ ├── ProviderSoundcloud.php │ │ ├── ProviderVideo.php │ │ └── ProviderYoutube.php │ ├── ModelSearch.php │ ├── Shaark │ │ ├── Concerns │ │ │ ├── ControlsComments.php │ │ │ ├── ControlsGlobalPrivacy.php │ │ │ ├── ControlsSettings.php │ │ │ └── HandleCustomSettings.php │ │ └── Shaark.php │ ├── UpdateChecker.php │ └── WebParser.php ├── Share.php ├── Story.php ├── Tag.php ├── User.php └── Wall.php ├── artisan ├── bootstrap ├── app.php └── cache │ └── .gitignore ├── changelog.md ├── composer.json ├── composer.lock ├── config ├── app.php ├── auth-checker.php ├── auth.php ├── backup.php ├── broadcasting.php ├── cache.php ├── captcha.php ├── database.php ├── filesystems.php ├── hashing.php ├── logging.php ├── mail.php ├── medialibrary.php ├── queue.php ├── scout.php ├── services.php ├── session.php ├── shaark.php └── view.php ├── database ├── .gitignore ├── factories │ ├── AlbumFactory.php │ ├── ChestFactory.php │ ├── LinkFactory.php │ ├── PostFactory.php │ ├── StoryFactory.php │ ├── UserFactory.php │ └── WallFactory.php ├── migrations │ ├── 2019_08_12_080000_create_users_table.php │ ├── 2019_08_20_100000_create_password_resets_table.php │ ├── 2019_08_22_120000_create_posts_table.php │ ├── 2019_08_22_130000_create_links_table.php │ ├── 2019_08_22_140000_create_stories_table.php │ ├── 2019_08_22_150000_create_tags_table.php │ ├── 2019_08_22_160000_create_chests_table.php │ ├── 2019_09_02_110000_add_archive_columns_to_links_table.php │ ├── 2019_09_02_120000_create_jobs_table.php │ ├── 2019_09_02_130000_create_failed_jobs_table.php │ ├── 2019_09_25_211017_create_secure_logins_table.php │ ├── 2019_09_26_153342_create_devices_table.php │ ├── 2019_09_26_153342_create_logins_table.php │ ├── 2019_10_07_140000_add_user_to_posts_table.php │ ├── 2019_10_10_110000_add_is_admin_to_users_table.php │ ├── 2019_10_21_140000_create_shares_table.php │ ├── 2019_10_22_190000_add_is_pinned_to_posts_table.php │ ├── 2019_11_02_190000_create_media_table.php │ ├── 2019_11_02_200000_create_albums_table.php │ ├── 2019_11_20_130000_create_comments_table.php │ ├── 2020_01_03_150000_create_walls_table.php │ ├── 2020_01_03_170000_update_logins_and_devices_table_user_relation.php │ └── 2020_06_17_060000_add_health_checks_to_links_table.php └── seeds │ ├── DatabaseSeeder.php │ └── LinkSeeder.php ├── docker-compose.yml ├── documentation ├── archiving.md ├── backup.md ├── comments.md ├── dependencies.md ├── docker.md ├── installation.md └── troubleshooting.md ├── package.json ├── phpunit.dusk.xml ├── public ├── .htaccess ├── css │ └── app.css ├── favicon.ico ├── fonts │ ├── IBMPlexSans-Bold.ttf │ ├── IBMPlexSans-Bold.woff │ ├── IBMPlexSans-Bold.woff2 │ ├── IBMPlexSans.ttf │ ├── IBMPlexSans.woff │ ├── IBMPlexSans.woff2 │ └── vendor │ │ ├── @fortawesome │ │ └── fontawesome-free │ │ │ ├── webfa-solid-900.eot │ │ │ ├── webfa-solid-900.svg │ │ │ ├── webfa-solid-900.ttf │ │ │ ├── webfa-solid-900.woff │ │ │ └── webfa-solid-900.woff2 │ │ └── mavon-editor │ │ └── dist │ │ ├── fontello.eot │ │ ├── fontello.svg │ │ ├── fontello.ttf │ │ ├── fontello.woff │ │ └── fontello.woff2 ├── images │ ├── logo-shaark.png │ ├── logo-shaarli.png │ └── vendor │ │ └── tui-editor │ │ └── dist │ │ ├── tui-editor-2x.png │ │ └── tui-editor.png ├── index.php ├── js │ ├── app.js │ ├── manifest.js │ └── vendor.js ├── mix-manifest.json └── web.config ├── readme.md ├── resources ├── js │ ├── app.js │ ├── components │ │ ├── AlbumCard.vue │ │ ├── AlbumForm.vue │ │ ├── Background.vue │ │ ├── CheckFeature.vue │ │ ├── ChestCard.vue │ │ ├── ChestForm.vue │ │ ├── ChestLines.vue │ │ ├── Comment.vue │ │ ├── Comments.vue │ │ ├── Confirm.vue │ │ ├── Flash.vue │ │ ├── HealthChecks.vue │ │ ├── LinkCard.vue │ │ ├── LinkForm.vue │ │ ├── Loader.vue │ │ ├── ManageArchives.vue │ │ ├── ManageTags.vue │ │ ├── ManageWalls.vue │ │ ├── Modal.vue │ │ ├── PasswordGenerator.vue │ │ ├── PreventBrowserUnload.vue │ │ ├── PurgeLogins.vue │ │ ├── Search.vue │ │ ├── Sharer.vue │ │ ├── StoryCard.vue │ │ ├── StoryForm.vue │ │ ├── TagLimiter.vue │ │ ├── Tags.vue │ │ ├── TempSharing.vue │ │ ├── UpdateNotifier.vue │ │ ├── UserForm.vue │ │ └── Users.vue │ ├── mixins │ │ ├── audit.js │ │ ├── auth.js │ │ ├── bus.js │ │ ├── copyToClipboard.js │ │ ├── formErrors.js │ │ ├── httpErrors.js │ │ └── i18n.js │ └── sw.js ├── lang │ ├── de.json │ ├── de │ │ ├── auth.php │ │ ├── pagination.php │ │ ├── passwords.php │ │ ├── shaark.php │ │ └── validation.php │ ├── en │ │ ├── auth.php │ │ ├── pagination.php │ │ ├── passwords.php │ │ ├── shaark.php │ │ └── validation.php │ ├── fr.json │ ├── fr │ │ ├── auth.php │ │ ├── pagination.php │ │ ├── passwords.php │ │ ├── shaark.php │ │ └── validation.php │ ├── ja.json │ ├── ja │ │ ├── auth.php │ │ ├── pagination.php │ │ ├── passwords.php │ │ ├── shaark.php │ │ └── validation.php │ ├── nl.json │ ├── nl │ │ ├── auth.php │ │ ├── pagination.php │ │ ├── passwords.php │ │ ├── shaark.php │ │ └── validation.php │ └── vendor │ │ └── backup │ │ ├── ar │ │ └── notifications.php │ │ ├── cs │ │ └── notifications.php │ │ ├── da │ │ └── notifications.php │ │ ├── de │ │ └── notifications.php │ │ ├── en │ │ └── notifications.php │ │ ├── es │ │ └── notifications.php │ │ ├── fa │ │ └── notifications.php │ │ ├── fr │ │ └── notifications.php │ │ ├── hi │ │ └── notifications.php │ │ ├── id │ │ └── notifications.php │ │ ├── it │ │ └── notifications.php │ │ ├── nl │ │ └── notifications.php │ │ ├── pl │ │ └── notifications.php │ │ ├── pt-BR │ │ └── notifications.php │ │ ├── ro │ │ └── notifications.php │ │ ├── ru │ │ └── notifications.php │ │ ├── tr │ │ └── notifications.php │ │ ├── uk │ │ └── notifications.php │ │ ├── zh-CN │ │ └── notifications.php │ │ └── zh-TW │ │ └── notifications.php ├── sass │ ├── _components.scss │ ├── _dark.scss │ ├── _variables.scss │ └── app.scss ├── screenshots │ └── home.jpg ├── ssl │ ├── dev.shaark.crt │ └── dev.shaark.key └── views │ ├── account.blade.php │ ├── album.blade.php │ ├── chest.blade.php │ ├── error.blade.php │ ├── errors │ ├── 401.blade.php │ ├── 403.blade.php │ ├── 404.blade.php │ ├── 419.blade.php │ ├── 429.blade.php │ ├── 500.blade.php │ └── 503.blade.php │ ├── feed │ ├── atom.blade.php │ └── rss.blade.php │ ├── form-album.blade.php │ ├── form-chest.blade.php │ ├── form-link.blade.php │ ├── form-story.blade.php │ ├── home.blade.php │ ├── layouts │ ├── app.blade.php │ ├── manage.blade.php │ ├── minimal.blade.php │ └── partials │ │ ├── footer.blade.php │ │ ├── head.blade.php │ │ ├── navbar.blade.php │ │ └── scripts.blade.php │ ├── link.blade.php │ ├── login-secure.blade.php │ ├── login.blade.php │ ├── manage │ ├── archives.blade.php │ ├── export.blade.php │ ├── links.blade.php │ ├── settings.blade.php │ ├── tags.blade.php │ ├── users.blade.php │ └── walls.blade.php │ ├── offline.blade.php │ ├── partials │ └── flash.blade.php │ ├── story.blade.php │ ├── tag.blade.php │ └── vendor │ └── pagination │ ├── bootstrap-4.blade.php │ └── simple-bootstrap-4.blade.php ├── routes ├── api.php ├── channels.php ├── console.php └── web.php ├── server.php ├── storage ├── app │ ├── .gitignore │ └── public │ │ └── .gitignore ├── framework │ ├── .gitignore │ ├── cache │ │ ├── .gitignore │ │ └── data │ │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ ├── testing │ │ └── .gitignore │ └── views │ │ └── .gitignore └── logs │ └── .gitignore ├── tests ├── Browser │ ├── AuthTest.php │ ├── BrowseTest.php │ ├── LinkFormTest.php │ ├── TempSharingTest.php │ ├── console │ │ └── .gitignore │ └── screenshots │ │ └── .gitignore ├── CreatesApplication.php ├── DuskTestCase.php ├── TestCase.php └── boostrap.php └── webpack.mix.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.yml] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME="Shaark" 2 | # "production" or "local" 3 | APP_ENV=local 4 | # generated with: php artisan key:generate 5 | APP_KEY= 6 | # set debug to false in production 7 | APP_DEBUG=true 8 | APP_URL=http://127.0.0.1:8000 9 | APP_TIMEZONE=Europe/Paris 10 | # "en", "fr", "de", "nl" or "ja" 11 | APP_LANG=en 12 | 13 | # others drivers such as "sqlite" are not supported 14 | DB_CONNECTION=mysql 15 | DB_HOST=127.0.0.1 16 | DB_PORT=3306 17 | DB_DATABASE=homestead 18 | DB_USERNAME=homestead 19 | DB_PASSWORD=secret 20 | 21 | # "file" or "redis" (if configured) 22 | CACHE_DRIVER=file 23 | # "sync" (local), "database" or "redis" (if configured) 24 | QUEUE_CONNECTION=sync 25 | # "file" or "redis" (if configured) 26 | SESSION_DRIVER=file 27 | SESSION_LIFETIME=120 28 | # "gd" or "imagick" (user in albums) 29 | IMAGE_DRIVER=gd 30 | 31 | # optional 32 | REDIS_HOST=127.0.0.1 33 | REDIS_PASSWORD=null 34 | REDIS_PORT=6379 35 | 36 | # "sendmail" or "smtp" 37 | MAIL_DRIVER=sendmail 38 | MAIL_HOST=127.0.0.1 39 | MAIL_PORT=1025 40 | MAIL_USERNAME=null 41 | MAIL_PASSWORD=null 42 | MAIL_ENCRYPTION=null 43 | MAIL_FROM_ADDRESS=hello@example.com 44 | MAIL_FROM_NAME="Shaark" 45 | 46 | # replace 'default-salt' with a random string 47 | HASHIDS_SALT=default-salt 48 | HASHIDS_MIN_LENGTH=10 49 | 50 | # "local" or "ftp" 51 | BACKUP_DRIVER=local 52 | # leave BACKUP_* empty if you're using the "local" driver 53 | BACKUP_HOST=ftp.example.com 54 | BACKUP_PORT=21 55 | BACKUP_USERNAME= 56 | BACKUP_PASSWORD= 57 | BACKUP_TIMEOUT=60 58 | BACKUP_SSL=false 59 | -------------------------------------------------------------------------------- /.env.testing: -------------------------------------------------------------------------------- 1 | APP_NAME="Shaark" 2 | APP_ENV=testing 3 | APP_KEY=base64:2Fevru/sI4EXg7a8hNn2+eJx1GGvywDWAsbCA/kMyvA= 4 | APP_DEBUG=true 5 | APP_URL=http://127.0.0.1:8000 6 | APP_TIMEZONE=Europe/Paris 7 | APP_LANG=en 8 | 9 | LOG_CHANNEL=stack 10 | 11 | DB_CONNECTION=testing 12 | 13 | BROADCAST_DRIVER=log 14 | CACHE_DRIVER=array 15 | QUEUE_CONNECTION=sync 16 | SESSION_DRIVER=file 17 | SESSION_LIFETIME=120 18 | SCOUT_DRIVER=null 19 | 20 | REDIS_HOST=127.0.0.1 21 | REDIS_PASSWORD=null 22 | REDIS_PORT=6379 23 | 24 | MAIL_DRIVER=array 25 | 26 | HASHIDS_SALT=default-salt 27 | HASHIDS_MIN_LENGTH=10 28 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.css linguist-vendored 3 | *.scss linguist-vendored 4 | *.js linguist-vendored 5 | CHANGELOG.md export-ignore 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /public/hot 3 | /public/storage 4 | /storage/*.json 5 | /storage/*.key 6 | /storage/*.index 7 | /storage/medialibrary 8 | /vendor 9 | .env 10 | .phpunit.result.cache 11 | Homestead.json 12 | Homestead.yaml 13 | npm-debug.log 14 | yarn-error.log 15 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | php: 2 | preset: laravel 3 | disabled: 4 | - unused_use 5 | finder: 6 | not-name: 7 | - index.php 8 | - server.php 9 | js: 10 | finder: 11 | not-name: 12 | - webpack.mix.js 13 | css: true 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7-alpine 2 | MAINTAINER Shaark contributors 3 | 4 | WORKDIR /app 5 | COPY . /app 6 | 7 | RUN apk add --no-cache --update openssl zip unzip oniguruma-dev zlib-dev libpng-dev libzip-dev postgresql-dev && \ 8 | curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer && \ 9 | docker-php-ext-install pdo mbstring gd exif zip sockets pdo_mysql pgsql pdo_pgsql && \ 10 | cp .env.example .env && \ 11 | \ 12 | sed -i s/DB_HOST=127.0.0.1/DB_HOST=mariadb/ .env && \ 13 | sed -i s/REDIS_HOST=127.0.0.1/REDIS_HOST=redis/ .env && \ 14 | sed -i s/APP_ENV=local/APP_ENV=production/ .env && \ 15 | sed -i s/APP_DEBUG=true/APP_DEBUG=false/ .env && \ 16 | sed -i s/CACHE_DRIVER=file/CACHE_DRIVER=redis/ .env && \ 17 | sed -i s/QUEUE_CONNECTION=sync/QUEUE_CONNECTION=redis/ .env && \ 18 | sed -i s/SESSION_DRIVER=file/SESSION_DRIVER=redis/ .env && \ 19 | sed -i s/REDIS_HOST=127.0.0.1/REDIS_HOST=redis/ .env && \ 20 | \ 21 | composer install --no-dev -o && \ 22 | php artisan optimize && \ 23 | php artisan view:clear && \ 24 | \ 25 | php artisan key:generate && \ 26 | php artisan storage:link && \ 27 | php artisan config:cache && \ 28 | php artisan migrate --seed 29 | 30 | CMD php artisan serve --host=0.0.0.0 --port=80 31 | EXPOSE 80 32 | -------------------------------------------------------------------------------- /app/Concerns/Models/Postable.php: -------------------------------------------------------------------------------- 1 | morphOne(Post::class, 'postable'); 21 | } 22 | 23 | public function scopeWithPrivate(Builder $query, $user = null): Builder 24 | { 25 | return $query->whereHas('post', function ($query) use ($user) { 26 | return $query->withPrivate($user); 27 | }); 28 | } 29 | 30 | public function toSearchableArray() 31 | { 32 | return [ 33 | 'title' => $this->title, 34 | 'content' => $this->content, 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Console/Commands/CleanFiles.php: -------------------------------------------------------------------------------- 1 | 'tmp/', 'search' => '/^(.*)$/', 'disk' => 'local'], 22 | ['path' => '/', 'search' => '/\.part$/', 'disk' => 'archives'], 23 | ]; 24 | 25 | foreach ($dirs as $dir) { 26 | $this->cleanDirectory($dir); 27 | } 28 | } 29 | 30 | protected function cleanDirectory(array $options): void 31 | { 32 | ['path' => $path, 'search' => $search, 'disk' => $disk] = $options; 33 | $files = Storage::disk($disk)->files($path); 34 | 35 | if ($this->option('verbose')) $this->info("Disk $disk:"); 36 | 37 | foreach ($files as $file) { 38 | $name = basename($file); 39 | 40 | if (preg_match('/^\./', $name)) { 41 | if ($this->option('verbose')) $this->comment("Ignore $name"); 42 | 43 | continue; 44 | } 45 | 46 | if (preg_match($search, $name)) { 47 | if ($this->option('verbose')) $this->comment("Delete $name"); 48 | 49 | Storage::disk($disk)->delete($file); 50 | 51 | continue; 52 | } 53 | 54 | if ($this->option('verbose')) $this->comment("Keep $name"); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/Console/Commands/DecryptChests.php: -------------------------------------------------------------------------------- 1 | getTable(); 22 | $count = DB::table($table)->count(); 23 | 24 | $bar = $this->output->createProgressBar($count); 25 | $bar->start(); 26 | 27 | DB::table($table) 28 | ->select('id', 'content') 29 | ->orderByDesc('created_at') 30 | ->chunk(10, function ($chests) use ($table, $bar) { 31 | foreach ($chests as $chest) { 32 | if (empty($chest->content)) { 33 | continue; 34 | } 35 | 36 | try { 37 | $value = decrypt($chest->content, false); 38 | } catch (\Exception $e) { 39 | unset($e); 40 | } 41 | 42 | if (! empty($value)) { 43 | $chest->content = $value; 44 | 45 | DB::table($table)->where('id', $chest->id)->update(['content' => $chest->content]); 46 | } 47 | } 48 | 49 | $bar->advance(10); 50 | }); 51 | 52 | $bar->finish(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/Console/Commands/EncryptChests.php: -------------------------------------------------------------------------------- 1 | getTable(); 22 | $count = DB::table($table)->count(); 23 | 24 | $bar = $this->output->createProgressBar($count); 25 | $bar->start(); 26 | 27 | DB::table($table) 28 | ->select('id', 'content') 29 | ->orderByDesc('created_at') 30 | ->chunk(10, function ($chests) use ($table, $bar) { 31 | foreach ($chests as $chest) { 32 | if (empty($chest->content)) { 33 | continue; 34 | } 35 | 36 | try { 37 | $value = json_decode($chest->content); 38 | } catch (\Exception $e) { 39 | unset($e); 40 | } 41 | 42 | if (! empty($value)) { 43 | $chest->content = encrypt(json_encode($value), false); 44 | 45 | DB::table($table)->where('id', $chest->id)->update(['content' => $chest->content]); 46 | } 47 | } 48 | 49 | $bar->advance(10); 50 | }); 51 | 52 | $bar->finish(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/Console/Commands/Install.php: -------------------------------------------------------------------------------- 1 | confirm('All existing data will be erased')) { 20 | $this->comment('Ok. Bye.'); 21 | return; 22 | } 23 | 24 | $this->comment('Migrations'); 25 | $this->callSilent('migrate:fresh'); 26 | 27 | if ($this->confirm('Default data?')) { 28 | $this->callSilent('db:seed'); 29 | exec('php artisan scout:import ' . \App\Link::class); 30 | } 31 | 32 | $name = $this->ask('User name?', 'Admin'); 33 | $email = $this->ask('User email?', 'shaark@example.com'); 34 | $pass = $this->ask('User pass?', 'secret'); 35 | 36 | $user = \App\User::count() ? \App\User::first() : new \App\User(); 37 | 38 | $user->fill([ 39 | 'name' => $name, 40 | 'email' => $email, 41 | 'password' => \Illuminate\Support\Facades\Hash::make($pass), 42 | 'api_token' => $pass === 'secret' ? 'api-token-secret' : \App\User::generateApiToken(), 43 | ]); 44 | 45 | $user->save(); 46 | $this->comment('User udpated'); 47 | 48 | $this->comment('Installation done'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/Console/Commands/LinkHealthChecks.php: -------------------------------------------------------------------------------- 1 | isWatched() 20 | ->lastCheckedBefore(now()->subDays(app(Shaark::class)->getLinkHealthChecksAge())) 21 | ->chunk(10, function ($links) { 22 | /** @var Link[]|Collection $links */ 23 | /** @var Link $link */ 24 | foreach ($links as $link) { 25 | LinkHealthCheck::dispatch($link); 26 | } 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Console/Commands/ResetForDemo.php: -------------------------------------------------------------------------------- 1 | callSilent('down', [ 20 | '--message' => 'App is being resetted', 21 | ]); 22 | 23 | $this->callSilent('migrate:fresh', [ 24 | '--force' => true, 25 | '--no-interaction' => true, 26 | '--seed' => true 27 | ]); 28 | 29 | $this->callSilent('up'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Events/LinkArchiveRequested.php: -------------------------------------------------------------------------------- 1 | link = $link; 26 | $this->provider = $provider; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Events/LinkHealthCheck.php: -------------------------------------------------------------------------------- 1 | link = $link; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | latest() 16 | ->get(); 17 | } 18 | 19 | public function headings(): array 20 | { 21 | return [ 22 | 'ID', 23 | __('Title'), 24 | __('Content'), 25 | __('Tags'), 26 | __('Date'), 27 | ]; 28 | } 29 | 30 | public function map($row): array 31 | { 32 | return [ 33 | $row->id, 34 | $row->title, 35 | $this->transformContent($row->content), 36 | $row->post->tags->pluck('name')->implode(', '), 37 | $row->created_at, 38 | ]; 39 | } 40 | 41 | public function transformContent(array $content): string 42 | { 43 | $output = ''; 44 | 45 | foreach ($content as $line) { 46 | if ($line->type === 'code') { 47 | $output .= $line->name . " :\n" . $line->value . "\n"; 48 | } else { 49 | $output .= $line->name . " : " . $line->value . "\n"; 50 | } 51 | } 52 | 53 | return $output; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/Exports/LinksExport.php: -------------------------------------------------------------------------------- 1 | latest() 16 | ->get(); 17 | } 18 | 19 | public function headings(): array 20 | { 21 | return [ 22 | 'ID', 23 | __('Title'), 24 | __('URL'), 25 | __('Content'), 26 | 'Extra', 27 | __('Is private?'), 28 | __('Tags'), 29 | __('Date'), 30 | ]; 31 | } 32 | 33 | public function map($row): array 34 | { 35 | return [ 36 | $row->id, 37 | $row->title, 38 | $row->url, 39 | $row->content, 40 | $row->extra, 41 | $row->post->is_private ? __('Yes') : __('No'), 42 | $row->post->tags->pluck('name')->implode(', '), 43 | $row->created_at, 44 | ]; 45 | } 46 | 47 | 48 | } 49 | -------------------------------------------------------------------------------- /app/Exports/StoriesExport.php: -------------------------------------------------------------------------------- 1 | latest() 16 | ->get(); 17 | } 18 | 19 | public function headings(): array 20 | { 21 | return [ 22 | 'ID', 23 | __('Title'), 24 | 'Slug', 25 | __('Content'), 26 | __('Is private?'), 27 | __('Tags'), 28 | __('Date'), 29 | ]; 30 | } 31 | 32 | public function map($row): array 33 | { 34 | return [ 35 | $row->id, 36 | $row->title, 37 | $row->slug, 38 | $row->content, 39 | $row->post->is_private ? __('Yes') : __('No'), 40 | $row->post->tags->pluck('name')->implode(', '), 41 | $row->created_at, 42 | ]; 43 | } 44 | 45 | 46 | } 47 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/AccountController.php: -------------------------------------------------------------------------------- 1 | middleware([ 14 | 'auth:api', 15 | 'demo' 16 | ]); 17 | } 18 | 19 | public function purge(Request $request) 20 | { 21 | DB::table('logins') 22 | ->where(['user_id' => $request->user()->id]) 23 | ->delete(); 24 | 25 | DB::table('devices') 26 | ->where(['user_id' => $request->user()->id]) 27 | ->delete(); 28 | 29 | return response()->json([ 30 | 'status' => 'purged', 31 | ]); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/Manage/ArchivesController.php: -------------------------------------------------------------------------------- 1 | middleware('demo'); 15 | } 16 | 17 | public function all(Request $request) 18 | { 19 | $archives = Link::whereNotNull('archive')->latest()->get(); 20 | 21 | return response()->json( 22 | LinkArchiveResource::collection($archives) 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/Manage/LinksHealthController.php: -------------------------------------------------------------------------------- 1 | middleware('demo'); 15 | } 16 | 17 | public function get(Request $request, string $type) 18 | { 19 | if (false === in_array($type, Link::HEALTH_STATUS)) { 20 | abort(404); 21 | } 22 | 23 | $links = Link::isWatched() 24 | ->healthStatusIs($type) 25 | ->withPrivate($request->user('api')) 26 | ->get(); 27 | 28 | return LinkResource::collection($links); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/Manage/TagsController.php: -------------------------------------------------------------------------------- 1 | middleware('demo')->except('all'); 15 | } 16 | 17 | public function all() 18 | { 19 | $tags = Tag::withCount('posts') 20 | ->orderByDesc('posts_count') 21 | ->get(); 22 | 23 | return response()->json($tags); 24 | } 25 | 26 | public function move(Request $request, string $from, string $to) 27 | { 28 | $posts = Post::withAnyTags($from)->get(); 29 | 30 | $posts->each(function ($item) use ($to) { 31 | $item->attachTag($to); 32 | }); 33 | 34 | return $this->delete($request, $from); 35 | } 36 | 37 | public function rename(Request $request, string $from, string $to) 38 | { 39 | $fromTag = Tag::named($from)->firstOrFail(); 40 | $toTag = Tag::named($to)->first(); 41 | 42 | if ($toTag) { 43 | response()->json([], 422); 44 | } 45 | 46 | $fromTag->name = $to; 47 | $fromTag->save(); 48 | 49 | return response()->json(); 50 | } 51 | 52 | public function delete(Request $request, string $tag) 53 | { 54 | $tag = Tag::findNamedOrCreate($tag); 55 | $tag->delete(); 56 | 57 | return response()->json(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/SearchController.php: -------------------------------------------------------------------------------- 1 | get('query'); 19 | $default_search = $shaark->getUseDefaultSearch(); 20 | 21 | if (mb_strlen($query) < 3) { 22 | abort(422); 23 | } 24 | 25 | $tags = (new ModelSearch(Tag::class, ['name'])) 26 | ->useDefaultSearch($default_search) 27 | ->search($query, 5); 28 | 29 | 30 | $posts = (new ModelSearch(Post::class, ['postable' => ['title', 'content']])) 31 | ->useDefaultSearch($default_search) 32 | ->withCallback(function ($query) { 33 | return $query->with('tags', 'postable') 34 | ->withPrivate(auth('api')->user()); 35 | }) 36 | ->search($query, 10); 37 | 38 | return [ 39 | 'posts' => PostResource::collection($posts), 40 | 'tags' => TagResource::collection($tags), 41 | ]; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/ShareController.php: -------------------------------------------------------------------------------- 1 | middleware('auth:api'); 16 | } 17 | 18 | public function get(Request $request, int $post_id) 19 | { 20 | Share::clearExpired(); 21 | $shares = Share::postIs($post_id)->get(); 22 | 23 | return response()->json([ 24 | 'shares' => ShareResource::collection($shares), 25 | ]); 26 | } 27 | 28 | public function store(Request $request, int $post_id) 29 | { 30 | $post = Post::findOrFail($post_id); 31 | 32 | $validated = $this->validate($request, [ 33 | 'expiration' => [ 34 | 'required', 35 | 'in:hour,hours,day,days,week,weeks,month', 36 | ] 37 | ]); 38 | 39 | $share = new Share(['post_id' => $post->id]); 40 | $share->setExpiration($validated['expiration']); 41 | $share->generateToken(); 42 | 43 | if ($share->save()) { 44 | return response()->json([ 45 | 'share' => new ShareResource($share), 46 | 'status' => 'created', 47 | ]); 48 | } 49 | 50 | return response()->json([ 51 | 'message' => __('Unable to create link for this content'), 52 | 'status' => 'error', 53 | ]); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/TagsController.php: -------------------------------------------------------------------------------- 1 | json( 14 | Tag::select('name') 15 | ->latest() 16 | ->get() 17 | ->pluck('name') 18 | ->toArray() 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Http/Controllers/ChestController.php: -------------------------------------------------------------------------------- 1 | middleware('auth'); 13 | } 14 | 15 | public function create(Request $request) 16 | { 17 | return view('form-chest')->with([ 18 | 'page_title' => __('Add chest'), 19 | ]); 20 | } 21 | 22 | public function edit(Request $request, int $id) 23 | { 24 | $chest = Chest::with('post.tags')->findOrFail($id); 25 | 26 | return view('form-chest')->with([ 27 | 'page_title' => __('Update chest'), 28 | 'chest' => $chest, 29 | ]); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | flash('alert', $message); 17 | session()->flash('level', $level); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Http/Controllers/FeedController.php: -------------------------------------------------------------------------------- 1 | with('postable') 24 | ->take(25) 25 | ->latest() 26 | ->get() 27 | )->toArray($request); 28 | }); 29 | 30 | return view(sprintf('feed/%s', $type))->with([ 31 | 'title' => $shaark->getName(), 32 | 'link' => route('home'), 33 | 'description' => __('All new content of :title', ['title' => $shaark->getName()]), 34 | 'language' => $shaark->getLocale(), 35 | 'pub_date' => count($items) ? $items[0]['created_at']->toRssString() : Carbon::now()->toRssString(), 36 | 'items' => $items, 37 | ]); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Http/Controllers/LinkArchiveController.php: -------------------------------------------------------------------------------- 1 | middleware('demo'); 15 | } 16 | 17 | public function download(Request $request, int $id) 18 | { 19 | /** @var Link $link */ 20 | $link = Link::findOrFail($id); 21 | 22 | if (false === $link->canDownloadArchive()) { 23 | abort(404); 24 | } 25 | 26 | $file = $link->archive; 27 | 28 | if ($file && Storage::disk('archives')->exists($file)) { 29 | return Storage::disk('archives')->download($file, sprintf('%s-%s', Str::slug($link->title), $file)); 30 | } 31 | 32 | $this->flash(__('Link archive doest not exist', 'error')); 33 | return redirect()->back(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Http/Controllers/LinkController.php: -------------------------------------------------------------------------------- 1 | middleware('auth'); 13 | } 14 | 15 | public function create(Request $request) 16 | { 17 | return view('form-link')->with([ 18 | 'page_title' => __('Add link'), 19 | 'query' => $request->query('url') 20 | ]); 21 | } 22 | 23 | public function edit(Request $request, int $id) 24 | { 25 | $link = Link::with('post.tags')->findOrFail($id); 26 | 27 | return view('form-link')->with([ 28 | 'page_title' => __('Update link'), 29 | 'link' => $link, 30 | ]); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Http/Controllers/Manage/ArchivesController.php: -------------------------------------------------------------------------------- 1 | middleware(['auth', 'demo']); 12 | } 13 | 14 | public function view() 15 | { 16 | return view('manage.archives')->with([ 17 | 'page_title' => __('Archives'), 18 | ]); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Http/Controllers/Manage/ExportController.php: -------------------------------------------------------------------------------- 1 | middleware('auth'); 17 | 18 | $this->middleware('demo')->except('form'); 19 | } 20 | 21 | public function form(Request $request) 22 | { 23 | return view('manage.export')->with([ 24 | 'page_title' => __('Export') 25 | ]); 26 | } 27 | 28 | public function export(Request $request) 29 | { 30 | $format = $request->get('format'); 31 | $formats = ['xlsx', 'csv']; 32 | 33 | $type = $request->get('type'); 34 | $types = [ 35 | 'links' => LinksExport::class, 36 | 'chests' => ChestsExport::class, 37 | 'stories' => StoriesExport::class, 38 | ]; 39 | 40 | if (false === array_key_exists($type, $types) 41 | || false === in_array($format, $formats) 42 | ) { 43 | $this->flash(__('Export type or format not recognized'), 'error'); 44 | return redirect()->back(); 45 | } 46 | 47 | $class = $types[$type]; 48 | 49 | return Excel::download( 50 | new $class, 51 | "{$type}.{$format}", 52 | $format == 'csv' ? \Maatwebsite\Excel\Excel::CSV : \Maatwebsite\Excel\Excel::XLSX 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/Http/Controllers/Manage/LinksHealthController.php: -------------------------------------------------------------------------------- 1 | middleware('auth'); 15 | 16 | $this->middleware('demo')->except('view'); 17 | } 18 | 19 | public function view(Shaark $shaark) 20 | { 21 | $stats = Cache::remember('health-checks', now()->addHour(), function () { 22 | return [ 23 | 'total' => Link::isWatched()->count(), 24 | 'live' => Link::isWatched()->healthStatusIs(Link::HEALTH_STATUS_LIVE)->count(), 25 | 'dead' => Link::isWatched()->healthStatusIs(Link::HEALTH_STATUS_DEAD)->count(), 26 | 'error' => Link::isWatched()->healthStatusIs(Link::HEALTH_STATUS_ERROR)->count(), 27 | 'redirect' => Link::isWatched()->healthStatusIs(Link::HEALTH_STATUS_REDIRECT)->count(), 28 | 'disabled' => Link::isNotWatched()->count(), 29 | ]; 30 | }); 31 | 32 | return view('manage.links')->with([ 33 | 'page_title' => __('Links'), 34 | 'enabled' => $shaark->getLinkHealthChecksEnabled(), 35 | 'stats' => $stats, 36 | ]); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Http/Controllers/Manage/SettingsController.php: -------------------------------------------------------------------------------- 1 | middleware('auth'); 15 | 16 | $this->middleware('demo')->except('form'); 17 | } 18 | 19 | public function form(Request $request) 20 | { 21 | return view('manage.settings')->with([ 22 | 'page_title' => __('Settings'), 23 | 'settings' => app('shaark')->getSettings(), 24 | ]); 25 | } 26 | 27 | public function store(StoreSettingsRequest $request, Shaark $shaark) 28 | { 29 | $validated = collect($request->validated()); 30 | 31 | $shaark 32 | ->setSettings($validated) 33 | ->cleanSettings(); 34 | 35 | $this->flash(__('Settings updated!'), 'success'); 36 | return redirect()->back(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Http/Controllers/Manage/TagsController.php: -------------------------------------------------------------------------------- 1 | middleware('auth'); 12 | 13 | $this->middleware('demo')->except('view'); 14 | } 15 | 16 | public function view() 17 | { 18 | return view('manage.tags')->with([ 19 | 'page_title' => __('Tags'), 20 | ]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Http/Controllers/Manage/UsersController.php: -------------------------------------------------------------------------------- 1 | middleware('auth'); 13 | } 14 | 15 | public function all(Request $request) 16 | { 17 | return view('manage.users')->with([ 18 | 'page_title' => __('Users'), 19 | ]); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Http/Controllers/Manage/WallsController.php: -------------------------------------------------------------------------------- 1 | middleware('auth'); 12 | 13 | $this->middleware('demo')->except('view'); 14 | } 15 | 16 | public function view() 17 | { 18 | return view('manage.walls')->with([ 19 | 'page_title' => __('Walls'), 20 | ]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Http/Controllers/SecureLoginController.php: -------------------------------------------------------------------------------- 1 | middleware('throttle:5,1')->only('check'); 15 | 16 | $this->middleware('demo')->except('form'); 17 | } 18 | 19 | public function form(Request $request, SecureLogin $secure) 20 | { 21 | return view('login-secure')->with([ 22 | 'token' => $secure->token, 23 | 'expires' => $secure->expires_at, 24 | ]); 25 | } 26 | 27 | public function check(Request $request, SecureLogin $secure) 28 | { 29 | $validated = $request->validate([ 30 | 'code' => 'required', 31 | ]); 32 | 33 | if ($secure->code === $validated['code']) { 34 | $request->session()->regenerate(); 35 | 36 | Auth::login($secure->user, $request->filled('remember')); 37 | SecureLogin::where('user_id', $secure->user_id)->delete(); 38 | 39 | return redirect('/'); 40 | } 41 | 42 | throw ValidationException::withMessages([ 43 | 'code' => [__("Invalid secure code")], 44 | ]); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/Http/Controllers/StaticController.php: -------------------------------------------------------------------------------- 1 | getIsPrivate()) { 15 | $content = "User-agent: *\nDisallow: /"; 16 | } 17 | 18 | return response()->make($content, 200, [ 19 | 'Content-Type' => 'text/plain; charset=UTF-8', 20 | 'Content-Lenght' => strlen($content), 21 | ]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Http/Controllers/StoryController.php: -------------------------------------------------------------------------------- 1 | middleware('auth'); 13 | } 14 | 15 | public function create(Request $request) 16 | { 17 | return view('form-story')->with([ 18 | 'page_title' => __('Add story'), 19 | ]); 20 | } 21 | 22 | public function edit(Request $request, int $id) 23 | { 24 | $story = Story::with('post.tags')->findOrFail($id); 25 | 26 | return view('form-story')->with([ 27 | 'page_title' => __('Update story'), 28 | 'story' => $story, 29 | ]); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Http/Middleware/Authenticate.php: -------------------------------------------------------------------------------- 1 | expectsJson()) { 18 | return route('login'); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Http/Middleware/BlockInDemoMode.php: -------------------------------------------------------------------------------- 1 | expectsJson()) { 14 | return response()->json([ 15 | 'status' => 'error', 16 | 'message' => __("This action is not available in demo mode") 17 | ], 401); 18 | } 19 | 20 | session()->flash('alert', __("This action is not available in demo mode")); 21 | session()->flash('level', 'info'); 22 | 23 | return redirect()->back(); 24 | } 25 | 26 | return $next($request); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Http/Middleware/CheckForGlobalPrivacy.php: -------------------------------------------------------------------------------- 1 | authorizeFromRequest($request)) { 12 | return $next($request); 13 | } 14 | 15 | return redirect()->route('login'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/CheckForMaintenanceMode.php: -------------------------------------------------------------------------------- 1 | is_admin) { 14 | return $next($request); 15 | } 16 | 17 | if ($request->expectsJson()) { 18 | return response()->json([ 19 | 'status' => 'error', 20 | 'message' => __("You can't access settings section") 21 | ], 401); 22 | } 23 | 24 | session()->flash('alert', __("You can't access settings section")); 25 | session()->flash('level', 'error'); 26 | 27 | return redirect()->back(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Http/Middleware/RedirectIfAuthenticated.php: -------------------------------------------------------------------------------- 1 | check()) { 21 | return redirect('/'); 22 | } 23 | 24 | return $next($request); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrimStrings.php: -------------------------------------------------------------------------------- 1 | post = Post::withPrivate($this->user('api')) 18 | ->findOrFail($this->route('id')); 19 | 20 | return $shaark->authorizedToAddComments($this); 21 | } 22 | 23 | public function rules() 24 | { 25 | $rules = [ 26 | 'content' => [ 27 | 'required', 28 | 'min:10', 29 | 'max:3000', 30 | ], 31 | 'reply' => [ 32 | 'nullable', 33 | function ($attribute, $value, $fail) { 34 | if (! empty($value)) { 35 | $comment = Comment::postIs($this->post->id) 36 | ->findOrFail($value); 37 | } 38 | }, 39 | ] 40 | ]; 41 | 42 | if (empty($this->user('api'))) { 43 | $rules['name'] = [ 44 | 'required', 45 | 'min:2', 46 | ]; 47 | 48 | $rules['email'] = [ 49 | 'required', 50 | 'email' 51 | ]; 52 | 53 | $rules['captcha'] = [ 54 | 'required', 55 | 'captcha_api:' . $this->key, 56 | ]; 57 | } 58 | 59 | return $rules; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/Http/Requests/Manage/StoreUserRequest.php: -------------------------------------------------------------------------------- 1 | check() && auth()->user()->is_admin; 12 | } 13 | 14 | public function rules() 15 | { 16 | return [ 17 | 'name' => [ 18 | 'required', 19 | 'min:2', 20 | 'max:255', 21 | ], 22 | 'email' => [ 23 | 'required', 24 | 'email', 25 | 'unique:users' 26 | ], 27 | 'password' => [ 28 | 'required', 29 | 'min:8', 30 | 'confirmed', 31 | ], 32 | 'is_admin' => [ 33 | 'nullable', 34 | ] 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Http/Requests/Manage/UpdateUserRequest.php: -------------------------------------------------------------------------------- 1 | check() && auth()->user()->is_admin; 13 | } 14 | 15 | public function rules() 16 | { 17 | $rules = [ 18 | 'name' => [ 19 | 'required', 20 | 'min:2', 21 | 'max:255', 22 | ], 23 | 'email' => [ 24 | 'required', 25 | 'email', 26 | Rule::unique('users', 'email')->ignore($this->route('id'), 'id'), 27 | ], 28 | 'password' => [ 29 | 'nullable', 30 | 'confirmed', 31 | 'min:8', 32 | ] 33 | ]; 34 | 35 | if (auth()->user()->id == $this->route('id')) { 36 | $rules['is_admin'] = [ 37 | 'required', 38 | 'in:1' 39 | ]; 40 | } else { 41 | $rules['is_admin'] = [ 42 | 'nullable', 43 | ]; 44 | } 45 | 46 | return $rules; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/Http/Requests/StoreAlbumRequest.php: -------------------------------------------------------------------------------- 1 | check(); 12 | } 13 | 14 | public function rules() 15 | { 16 | return [ 17 | 'title' => [ 18 | 'required', 19 | 'min:2', 20 | 'max:255', 21 | ], 22 | 'content' => [ 23 | 'nullable', 24 | 'max:10000', 25 | ], 26 | 'is_private' => [ 27 | 'nullable', 28 | ], 29 | 'is_pinned' => [ 30 | 'nullable', 31 | ], 32 | 'uploaded' => [ 33 | 'nullable', 34 | 'array', 35 | ], 36 | 'uploaded.*' => [ 37 | 'temp_image', 38 | ], 39 | 'images' => [ 40 | 'nullable', 41 | 'array', 42 | ], 43 | 'images.*.order' => [ 44 | 'numeric', 45 | 'min:0', 46 | 'max:9999' 47 | ], 48 | 'tags' => [ 49 | 'nullable', 50 | 'array', 51 | 'max:10', 52 | ], 53 | 'tags.*' => [ 54 | 'min:1', 55 | 'max:30', 56 | ], 57 | ]; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/Http/Requests/StoreAlbumUploadRequest.php: -------------------------------------------------------------------------------- 1 | check(); 13 | } 14 | 15 | public function rules() 16 | { 17 | return [ 18 | 'filepond' => [ 19 | 'required', 20 | 'image', 21 | 'mimetypes:' . implode(',', Album::$mimes), 22 | ] 23 | ]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Http/Requests/StoreChestRequest.php: -------------------------------------------------------------------------------- 1 | check(); 12 | } 13 | 14 | public function rules() 15 | { 16 | return [ 17 | 'title' => [ 18 | 'required', 19 | 'min:2', 20 | 'max:255', 21 | ], 22 | 'content' => [ 23 | 'array', 24 | ], 25 | 'is_pinned' => [ 26 | 'nullable', 27 | ], 28 | 'tags' => [ 29 | 'nullable', 30 | 'array', 31 | 'max:10', 32 | ], 33 | 'tags.*' => [ 34 | 'min:1', 35 | 'max:30', 36 | ], 37 | ]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Http/Requests/StoreLinkRequest.php: -------------------------------------------------------------------------------- 1 | check(); 12 | } 13 | 14 | public function rules() 15 | { 16 | return [ 17 | 'title' => [ 18 | 'required', 19 | 'min:2', 20 | 'max:255', 21 | ], 22 | 'content' => [ 23 | 'nullable', 24 | 'max:10000', 25 | ], 26 | 'url' => [ 27 | 'required', 28 | 'url', 29 | ], 30 | 'is_watched' => [ 31 | 'nullable', 32 | ], 33 | 'is_private' => [ 34 | 'nullable', 35 | ], 36 | 'is_pinned' => [ 37 | 'nullable', 38 | ], 39 | 'tags' => [ 40 | 'nullable', 41 | 'array', 42 | 'max:10', 43 | ], 44 | 'tags.*' => [ 45 | 'min:1', 46 | 'max:30', 47 | ], 48 | ]; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/Http/Requests/StoreSettingsRequest.php: -------------------------------------------------------------------------------- 1 | check(); 12 | } 13 | 14 | public function rules() 15 | { 16 | $config = collect(app('shaark')->getSettingsConfig()); 17 | 18 | return $config->transform(function ($item, $key) { 19 | return [ 20 | 'key' => $key, 21 | 'rules' => $item['rules'], 22 | ]; 23 | })->pluck('rules', 'key')->toArray(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Http/Requests/StoreStoryRequest.php: -------------------------------------------------------------------------------- 1 | check(); 13 | } 14 | 15 | public function rules() 16 | { 17 | $unique = Rule::unique('stories'); 18 | 19 | if ($this->route('id')) { 20 | $unique->ignore($this->route('id'), 'id'); 21 | } 22 | 23 | return [ 24 | 'title' => [ 25 | 'required', 26 | 'min:2', 27 | 'max:255', 28 | ], 29 | 'slug' => [ 30 | 'required', 31 | $unique, 32 | 'max:255', 33 | ], 34 | 'content' => [ 35 | 'nullable', 36 | ], 37 | 'is_private' => [ 38 | 'nullable', 39 | ], 40 | 'is_pinned' => [ 41 | 'nullable', 42 | ], 43 | 'tags' => [ 44 | 'nullable', 45 | 'array', 46 | 'max:10', 47 | ], 48 | 'tags.*' => [ 49 | 'min:1', 50 | 'max:30', 51 | ], 52 | ]; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/Http/Requests/UpdateUserAccount.php: -------------------------------------------------------------------------------- 1 | check(); 12 | } 13 | 14 | public function rules() 15 | { 16 | return [ 17 | 'name' => [ 18 | 'required', 19 | 'min:2', 20 | 'max:60' 21 | ], 22 | 'email' => [ 23 | 'required', 24 | 'email:rfc,dns', 25 | 'max:100' 26 | ] 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Http/Requests/UpdateUserPassword.php: -------------------------------------------------------------------------------- 1 | check(); 13 | } 14 | 15 | public function rules() 16 | { 17 | return [ 18 | 'new_password' => [ 19 | 'required', 20 | 'min:8', 21 | 'confirmed', 22 | ], 23 | 'current_password' => [ 24 | 'required', 25 | 'different:new_password', 26 | function ($attribute, $value, $fail) { 27 | if (false === Hash::check($value, auth()->user()->getAuthPassword())) { 28 | $fail(__('Current password is invalid.')); 29 | } 30 | } 31 | ] 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Http/Resources/ChestResource.php: -------------------------------------------------------------------------------- 1 | $this->title, 14 | 'content' => $this->content, 15 | 'url' => $this->permalink, 16 | 'permalink' => $this->permalink, 17 | 'is_private' => true, 18 | 'is_pinned' => $this->post->is_pinned, 19 | 'date_formated' => $this->created_at->diffForHumans(), 20 | 'tags' => $this->post->tags->pluck('name')->toArray(), 21 | $this->mergeWhen(Auth::check(), [ 22 | 'editable' => true, 23 | 'url_store' => route('api.chest.store'), 24 | 'url_edit' => route('chest.edit', $this->id), 25 | 'url_update' => route('api.chest.update', $this->id), 26 | 'url_delete' => route('api.chest.delete', $this->id), 27 | 'url_share' => route('api.share', $this->post->id), 28 | ]) 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Http/Resources/CommentResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 13 | 'comment_id' => $this->comment_id, 14 | 'content' => $this->content, 15 | 'name' => $this->user->name, 16 | 'is_visible' => $this->is_visible, 17 | 'date_formated' => $this->created_at->diffForHumans(), 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Http/Resources/LinkArchiveResource.php: -------------------------------------------------------------------------------- 1 | $this->title, 15 | 'permalink' => $this->permalink, 16 | 'filename' => $this->archive, 17 | 'size' => Format::humanReadableSize(Storage::disk('archives')->size($this->archive)), 18 | 'extension' => last(explode('.', $this->archive)), 19 | 'date_formated' => $this->created_at->diffForHumans(), 20 | 'url_archive' => route('api.link.archive', $this->id), 21 | 'url_download' => route('link.archive.download', $this->id), 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Http/Resources/ShareResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 13 | 'post_id' => $this->post_id, 14 | 'url' => $this->url, 15 | 'expires_at' => $this->expires_at->diffForHumans(), 16 | 'created_at' => $this->created_at->diffForHumans(), 17 | ]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Http/Resources/StoryResource.php: -------------------------------------------------------------------------------- 1 | $this->title, 14 | 'slug' => $this->slug, 15 | 'content' => $this->content, 16 | 'url' => $this->url, 17 | 'is_private' => $this->post->is_private, 18 | 'is_pinned' => $this->post->is_pinned, 19 | 'date_formated' => $this->created_at->diffForHumans(), 20 | 'tags' => $this->post->tags->pluck('name')->toArray(), 21 | $this->mergeWhen(Auth::check(), [ 22 | 'editable' => true, 23 | 'url_store' => route('api.story.store'), 24 | 'url_edit' => route('story.edit', $this->id), 25 | 'url_update' => route('api.story.update', $this->id), 26 | 'url_delete' => route('api.story.delete', $this->id), 27 | 'url_share' => route('api.share', $this->post->id), 28 | ]) 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Http/Resources/TagResource.php: -------------------------------------------------------------------------------- 1 | $this->name, 13 | 'url' => $this->url, 14 | 'count' => $this->links_count, 15 | 'created_at' => $this->created_at, 16 | ]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Http/Resources/UserResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 14 | 'name' => $this->name, 15 | 'email' => $this->email, 16 | 'is_admin' => $this->is_admin, 17 | 'created_at' => $this->created_at, 18 | $this->mergeWhen(Auth::check(), function () { 19 | return [ 20 | 'url_update' => route('api.manage.users.update', $this->id), 21 | 'url_delete' => route('api.manage.users.delete', $this->id), 22 | ]; 23 | }) 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Listeners/CheckLinkHealth.php: -------------------------------------------------------------------------------- 1 | info(sprintf('Checking link health %d.', $event->link->id)); 15 | 16 | $link = $event->link; 17 | 18 | try { 19 | $response = (new Client())->request('GET', $link->url, [ 20 | 'headers' => [ 21 | 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36', 22 | ], 23 | 'http_errors' => false, 24 | 'verify' => false, 25 | 'timeout' => 5, 26 | ]); 27 | 28 | $link->http_status = $response->getStatusCode(); 29 | } catch (RequestException $exception) { 30 | // Might happen when the domain has expired 31 | $link->http_status = 500; 32 | } finally { 33 | $link->http_checked_at = now(); 34 | $link->save(); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Listeners/MakeLinkArchive.php: -------------------------------------------------------------------------------- 1 | info(sprintf('Archiving link %d with %s driver.', $event->link->id, $event->provider)); 16 | 17 | $link = $event->link; 18 | $file = LinkArchive::archive($link->url, $event->provider); 19 | 20 | if (Storage::disk('archives')->exists($file)) { 21 | $link->archive = $file; 22 | $link->save(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Login.php: -------------------------------------------------------------------------------- 1 | subject(__('shaark.mails.check.title')) 23 | ->line(__('shaark.mails.check.message', ['name' => config('app.name')])); 24 | } 25 | 26 | public function toArray($notifiable) 27 | { 28 | return []; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Notifications/NewComment.php: -------------------------------------------------------------------------------- 1 | comment = $comment; 21 | } 22 | 23 | public function via($notifiable) 24 | { 25 | return ['mail']; 26 | } 27 | 28 | public function toMail($notifiable) 29 | { 30 | return (new MailMessage) 31 | ->subject(__('shaark.mails.comment.title')) 32 | ->line(__('shaark.mails.comment.message', [ 33 | 'name' => $this->comment->user->name, 34 | 'email' => $this->comment->user->email, 35 | 'post' => $this->comment->post->postable->title, 36 | ])) 37 | ->action(__('shaark.mails.comment.action'), $this->comment->post->postable->permalink); 38 | } 39 | 40 | public function toArray($notifiable) 41 | { 42 | return []; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Notifications/NewUnmoderatedComment.php: -------------------------------------------------------------------------------- 1 | comment = $comment; 21 | } 22 | 23 | public function via($notifiable) 24 | { 25 | return ['mail']; 26 | } 27 | 28 | public function toMail($notifiable) 29 | { 30 | return (new MailMessage) 31 | ->subject(__('shaark.mails.unmoderated.title')) 32 | ->line(__('shaark.mails.unmoderated.message', [ 33 | 'name' => $this->comment->user->name, 34 | 'email' => $this->comment->user->email, 35 | 'post' => $this->comment->post->postable->title, 36 | ])) 37 | ->action(__('shaark.mails.unmoderated.action'), $this->comment->post->postable->permalink); 38 | } 39 | 40 | public function toArray($notifiable) 41 | { 42 | return []; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Notifications/SecureLoginCode.php: -------------------------------------------------------------------------------- 1 | secure = $secure; 21 | } 22 | 23 | public function via($notifiable) 24 | { 25 | return ['mail']; 26 | } 27 | 28 | public function toMail($notifiable) 29 | { 30 | return (new MailMessage) 31 | ->subject(__('shaark.mails.2fa.title')) 32 | ->line(__('shaark.mails.2fa.message', ['code' => $this->secure->code])) 33 | ->action(__('shaark.mails.2fa.button'), sprintf('%s?code=%s', route('login.secure', $this->secure), $this->secure->code)); 34 | } 35 | 36 | public function toArray($notifiable) 37 | { 38 | return []; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Observers/CommentObserver.php: -------------------------------------------------------------------------------- 1 | getCommentsNotification(); 15 | $users = User::isAdmin()->get(); 16 | 17 | $users->each(function (User $user) use ($notification, $comment) { 18 | if ($comment->user_id === $user->id) { 19 | return; 20 | } 21 | 22 | if ($notification !== 'disabled' && false == $comment->is_visible) { 23 | $user->notifyNow(new NewUnmoderatedComment($comment)); 24 | } else if ($notification === 'all' && $comment->is_visible) { 25 | $user->notifyNow(new NewComment($comment)); 26 | } 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Observers/TagObserver.php: -------------------------------------------------------------------------------- 1 | where('tag_id', $tag->id) 14 | ->delete(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/Observers/WallObserver.php: -------------------------------------------------------------------------------- 1 | is_default) { 12 | Wall::where('is_default', true) 13 | ->where('id', '!=', $wall->id) 14 | ->update([ 15 | 'is_default' => false, 16 | ]); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Providers/AuthServiceProvider.php: -------------------------------------------------------------------------------- 1 | 'App\Policies\ModelPolicy', 13 | ]; 14 | 15 | public function boot() 16 | { 17 | $this->registerPolicies(); 18 | 19 | Gate::define('restricted', function (?User $user) { 20 | if (app('shaark')->getIsPrivate() === false) { 21 | return true; 22 | } 23 | 24 | return !empty($user); 25 | }); 26 | 27 | Gate::define('comments.see', function (?User $user) { 28 | return app('shaark')->authorizedToSeeComments(request()); 29 | }); 30 | 31 | Gate::define('comments.add', function (?User $user) { 32 | return app('shaark')->authorizedToAddComments(request()); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Providers/BroadcastServiceProvider.php: -------------------------------------------------------------------------------- 1 | [ 12 | \App\Listeners\MakeLinkArchive::class 13 | ], 14 | \App\Events\LinkHealthCheck::class => [ 15 | \App\Listeners\CheckLinkHealth::class 16 | ], 17 | \Illuminate\Database\Events\MigrationEnded::class => [ 18 | \App\Listeners\UpdateDatabase::class 19 | ], 20 | ]; 21 | 22 | public function boot() 23 | { 24 | parent::boot(); 25 | 26 | \App\Comment::observe(\App\Observers\CommentObserver::class); 27 | \App\Tag::observe(\App\Observers\TagObserver::class); 28 | \App\Wall::observe(\App\Observers\WallObserver::class); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | findOrFail($value); 19 | }); 20 | } 21 | 22 | public function map() 23 | { 24 | $this->mapApiRoutes(); 25 | $this->mapWebRoutes(); 26 | } 27 | 28 | protected function mapWebRoutes() 29 | { 30 | Route::middleware('web') 31 | ->namespace($this->namespace) 32 | ->group(base_path('routes/web.php')); 33 | } 34 | 35 | protected function mapApiRoutes() 36 | { 37 | Route::prefix('api') 38 | ->middleware('api') 39 | ->name('api.') 40 | ->namespace($this->namespace . '\Api') 41 | ->group(base_path('routes/api.php')); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/SecureLogin.php: -------------------------------------------------------------------------------- 1 | belongsTo(User::class); 25 | } 26 | 27 | public function scopeIsNotExpired(Builder $query): Builder 28 | { 29 | return $query->where('expires_at', '>=', Carbon::now()->toDateTimeString()); 30 | } 31 | 32 | public function getRouteKeyName(): string 33 | { 34 | return 'token'; 35 | } 36 | 37 | /** 38 | * @param Authenticatable|User $user 39 | * @return static 40 | */ 41 | public static function createForUser($user): self 42 | { 43 | $code_length = app('shaark')->getSecureCodeLength(); 44 | $expire_minutes = app('shaark')->getSecureCodeExpires(); 45 | 46 | $model = new static(); 47 | $model->user_id = $user->id; 48 | $model->token = Str::random(100); 49 | $model->code = Str::random($code_length); 50 | $model->expires_at = Carbon::now()->addMinutes($expire_minutes); 51 | $model->save(); 52 | 53 | return $model; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/Services/Hashid.php: -------------------------------------------------------------------------------- 1 | hashids = new Hashids( 13 | $config['salt'], 14 | $config['min'], 15 | 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890' 16 | ); 17 | } 18 | 19 | public function encode($data): string 20 | { 21 | return $this->hashids->encode($data); 22 | } 23 | 24 | public function decode($data) 25 | { 26 | return $this->hashids->decode($data); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Services/LinkArchive/BaseProvider.php: -------------------------------------------------------------------------------- 1 | url = $url; 13 | } 14 | 15 | abstract public function isEnabled(): bool; 16 | 17 | abstract public function canArchive(): bool; 18 | 19 | abstract public function makeArchive(): ?string; 20 | } 21 | -------------------------------------------------------------------------------- /app/Services/LinkArchive/LinkArchive.php: -------------------------------------------------------------------------------- 1 | YoutubeDlProvider::class, 10 | 'pdf' => PuppeteerProvider::class, 11 | ]; 12 | 13 | public static function availableFor(string $url): array 14 | { 15 | $providers = []; 16 | 17 | foreach (self::$providers as $type => $provider) { 18 | /** @var BaseProvider $provider */ 19 | $provider = new $provider($url); 20 | 21 | if ($provider->isEnabled() && $provider->canArchive()) { 22 | $providers[] = $type; 23 | } 24 | } 25 | 26 | return $providers; 27 | } 28 | 29 | public static function archive(string $url, string $provider): ?string 30 | { 31 | if (false === array_key_exists($provider, self::$providers)) { 32 | throw new \RuntimeException("Unrecognized provider: $provider"); 33 | } 34 | 35 | $class = self::$providers[$provider]; 36 | /** @var BaseProvider $provider */ 37 | $provider = new $class($url); 38 | 39 | return $provider->makeArchive(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Services/LinkArchive/PuppeteerProvider.php: -------------------------------------------------------------------------------- 1 | url) . '.pdf'; 12 | $filename = sprintf('app/archives/%s', $name); 13 | 14 | try { 15 | $puppeteer = new Puppeteer([ 16 | 'executable_path' => app('shaark')->getNodeBin() 17 | ]); 18 | 19 | $browser = $puppeteer->launch([ 20 | 'ignoreHTTPSErrors' => true, 21 | ]); 22 | 23 | $page = $browser->newPage(); 24 | $page->goto($this->url); 25 | $page->emulateMedia('screen'); 26 | 27 | $page->pdf([ 28 | 'path' => storage_path($filename), 29 | 'width' => app('shaark')->getArchivePdfWidth(), 30 | 'height' => app('shaark')->getArchivePdfHeight(), 31 | 'printBackground' => true, 32 | 'preferCSSPageSize' => true, 33 | 'margin' => [ 34 | 'top' => 0, 35 | 'bottom' => 0, 36 | 'left' => 0, 37 | 'right' => 0, 38 | ] 39 | ]); 40 | 41 | $browser->close(); 42 | } catch (\Exception $e) { 43 | throw new \RuntimeException("Unable to create link pdf archive", 0, $e); 44 | } 45 | 46 | return $name; 47 | } 48 | 49 | public function isEnabled(): bool 50 | { 51 | return app('shaark')->getLinkArchivePdf() === true; 52 | } 53 | 54 | public function canArchive(): bool 55 | { 56 | return true; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/Services/LinkPreview/BaseProvider.php: -------------------------------------------------------------------------------- 1 | url = $url; 13 | } 14 | 15 | abstract public function canPreview(): bool; 16 | 17 | abstract public function getPreview(): ?string; 18 | 19 | public function makeRequest(?string $url): ?string 20 | { 21 | $url = $url ?? $this->url; 22 | 23 | try { 24 | $curl = curl_init(); 25 | 26 | curl_setopt($curl, CURLOPT_URL, $url); 27 | curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); 28 | 29 | $content = curl_exec($curl); 30 | curl_close($curl); 31 | 32 | return $content; 33 | } catch (\Exception $e) { 34 | unset($e); 35 | } 36 | 37 | return null; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Services/LinkPreview/LinkPreview.php: -------------------------------------------------------------------------------- 1 | url = $url; 21 | } 22 | 23 | public static function preview(string $url): ?string 24 | { 25 | $provider = (new static($url))->getProvider(); 26 | 27 | if ($provider) { 28 | return $provider->getPreview(); 29 | } 30 | 31 | return null; 32 | } 33 | 34 | public function getProvider(): ?BaseProvider 35 | { 36 | foreach ($this->providers as $provider) 37 | { 38 | /** @var BaseProvider $provider */ 39 | $provider = new $provider($this->url); 40 | 41 | if ($provider->canPreview()) { 42 | return $provider; 43 | } 44 | } 45 | 46 | return null; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/Services/LinkPreview/ProviderImage.php: -------------------------------------------------------------------------------- 1 | regex, $this->url) != false; 13 | } 14 | 15 | public function getPreview(): ?string 16 | { 17 | if (preg_match($this->regex, $this->url)) { 18 | return str_replace('{url}', $this->url, ''); 19 | } 20 | 21 | return null; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Services/LinkPreview/ProviderImgur.php: -------------------------------------------------------------------------------- 1 | regex, $this->url) != false; 13 | } 14 | 15 | public function getPreview(): ?string 16 | { 17 | preg_match($this->regex, $this->url, $matches); 18 | $id = $matches[1]; 19 | 20 | if ($id) { 21 | return str_replace('{id}', $id, '
'); 22 | } 23 | 24 | return null; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /app/Services/LinkPreview/ProviderSoundcloud.php: -------------------------------------------------------------------------------- 1 | url) != false; 10 | } 11 | 12 | public function getPreview(): ?string 13 | { 14 | $url = sprintf('http://soundcloud.com/oembed?format=json&url=%s&iframe=true', urlencode($this->url)); 15 | 16 | try { 17 | $content = json_decode(file_get_contents($url)); 18 | } catch (\Exception $e) { 19 | unset($e); 20 | return null; 21 | } 22 | 23 | return str_replace( 24 | ['visual=true', 'show_artwork=true', 'height="400"'], 25 | ['visual=false', 'show_artwork=false', 'height="140"'], 26 | $content->html); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /app/Services/LinkPreview/ProviderVideo.php: -------------------------------------------------------------------------------- 1 | regex, $this->url) != false; 13 | } 14 | 15 | public function getPreview(): ?string 16 | { 17 | return str_replace('{url}', $this->url, ''); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /app/Services/LinkPreview/ProviderYoutube.php: -------------------------------------------------------------------------------- 1 | regex, $this->url) != false; 13 | } 14 | 15 | public function getPreview(): ?string 16 | { 17 | preg_match($this->regex, $this->url, $matches); 18 | $id = $matches[1]; 19 | 20 | if ($id) { 21 | return '
'; 22 | } 23 | 24 | return null; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Services/Shaark/Concerns/ControlsComments.php: -------------------------------------------------------------------------------- 1 | getCommentsEnabled()) { 16 | return false; 17 | } 18 | 19 | $user = $this->getRequestUser(); 20 | 21 | if ($user && $user->exists) { 22 | return true; 23 | } 24 | 25 | return $this->getCommentsGuestView(); 26 | } 27 | 28 | public function authorizedToAddComments(Request $request): bool 29 | { 30 | if (false === $this->getCommentsEnabled()) { 31 | return false; 32 | } 33 | 34 | $user = $this->getRequestUser(); 35 | 36 | if ($user && $user->exists) { 37 | return true; 38 | } 39 | 40 | return $this->getCommentsGuestAdd(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/Services/Shaark/Concerns/ControlsGlobalPrivacy.php: -------------------------------------------------------------------------------- 1 | getIsPrivate() === false) { 18 | return true; 19 | } 20 | 21 | $user = $this->getRequestUser(); 22 | 23 | if (! empty($user) || $this->requestAuthorizedForGlobalPrivacy($request)) { 24 | return true; 25 | } 26 | 27 | return false; 28 | } 29 | 30 | public function requestAuthorizedForGlobalPrivacy(Request $request): bool 31 | { 32 | $excepts = [ 33 | 'login', 34 | 'login/secure/*', 35 | 'password/*', 36 | 'logout', 37 | 'shared/*', 38 | 'robots.txt', 39 | 'manifest.json', 40 | 'sw.js', 41 | ]; 42 | 43 | foreach ($excepts as $except) { 44 | if ($except !== '/') { 45 | $except = trim($except, '/'); 46 | } 47 | 48 | if ($request->fullUrlIs($except) || $request->is($except)) { 49 | return true; 50 | } 51 | } 52 | 53 | return false; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/Story.php: -------------------------------------------------------------------------------- 1 | url; 27 | } 28 | 29 | public function getUrlAttribute(): string 30 | { 31 | return route('story.view', $this->slug); 32 | } 33 | 34 | public function scopeSlugIs(Builder $query, string $slug): Builder 35 | { 36 | return $query->where('slug', $slug); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Tag.php: -------------------------------------------------------------------------------- 1 | morphedByMany(Post::class, 'taggable'); 23 | } 24 | 25 | public static function findNamedOrCreate(string $name) 26 | { 27 | return static::firstOrCreate(['name' => Str::slug($name)]); 28 | 29 | } 30 | 31 | public function getUrlAttribute(): string 32 | { 33 | return route('tag', $this->name); 34 | } 35 | 36 | public function scopeNamed(Builder $query, string $name): Builder 37 | { 38 | return $query->where('name', '=', $name); 39 | } 40 | 41 | public function scopeWithPostsFor(Builder $query, Request $request): Builder 42 | { 43 | return $query 44 | ->withCount(['posts' => function (Builder $query) use ($request) { 45 | $query->withPrivate($request); 46 | }]) 47 | ->whereHas('posts', function (Builder $query) use ($request) { 48 | $query->withPrivate($request); 49 | }, '>', 0); 50 | } 51 | 52 | public function toSearchableArray() 53 | { 54 | return [ 55 | 'id' => $this->id, 56 | 'name' => $this->name 57 | ]; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/User.php: -------------------------------------------------------------------------------- 1 | 'datetime', 33 | 'is_admin' => 'bool', 34 | ]; 35 | 36 | public function posts(): HasMany 37 | { 38 | return $this->hasMany(Post::class); 39 | } 40 | 41 | public function scopeIsAdmin(Builder $query): Builder 42 | { 43 | return $query->where('is_admin', 1); 44 | } 45 | 46 | public static function generateApiToken(): string 47 | { 48 | return Str::random(128); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /config/auth-checker.php: -------------------------------------------------------------------------------- 1 | env('AUTH_CHECKER_THROTTLE', 0), 7 | 'device_matching_attributes' => [ 8 | # Ex: OS X, Windows, ... 9 | 'platform', 10 | # Ex: 10_12_2, 8, ... 11 | 'platform_version', 12 | # Ex: Chrome, Firefox, ... 13 | 'browser', 14 | # Ex: 42.0.2311.135, 37.0, ... 15 | //'browser_version', 16 | ], 17 | 'login_column' => 'email', 18 | 'models' => [ 19 | # Ex: App\Models\Device (default: Lab404\AuthChecker\Models\Device) 20 | 'device' => null, 21 | # Ex: App\Models\Login (default: Lab404\AuthChecker\Models\Login) 22 | 'login' => Login::class, 23 | ] 24 | ]; 25 | -------------------------------------------------------------------------------- /config/auth.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'guard' => 'web', 6 | 'passwords' => 'users', 7 | ], 8 | 'guards' => [ 9 | 'web' => [ 10 | 'driver' => 'session', 11 | 'provider' => 'users', 12 | ], 13 | 'api' => [ 14 | 'driver' => 'token', 15 | 'provider' => 'users', 16 | 'hash' => false, 17 | ], 18 | ], 19 | 'providers' => [ 20 | 'users' => [ 21 | 'driver' => 'eloquent', 22 | 'model' => App\User::class, 23 | ], 24 | ], 25 | 'passwords' => [ 26 | 'users' => [ 27 | 'provider' => 'users', 28 | 'table' => 'password_resets', 29 | 'expire' => 60, 30 | ], 31 | ], 32 | ]; 33 | -------------------------------------------------------------------------------- /config/broadcasting.php: -------------------------------------------------------------------------------- 1 | env('BROADCAST_DRIVER', 'null'), 5 | 'connections' => [ 6 | 'pusher' => [ 7 | 'driver' => 'pusher', 8 | 'key' => env('PUSHER_APP_KEY'), 9 | 'secret' => env('PUSHER_APP_SECRET'), 10 | 'app_id' => env('PUSHER_APP_ID'), 11 | 'options' => [ 12 | 'cluster' => env('PUSHER_APP_CLUSTER'), 13 | 'encrypted' => true, 14 | ], 15 | ], 16 | 'redis' => [ 17 | 'driver' => 'redis', 18 | 'connection' => 'default', 19 | ], 20 | 'log' => [ 21 | 'driver' => 'log', 22 | ], 23 | 'null' => [ 24 | 'driver' => 'null', 25 | ], 26 | ], 27 | ]; 28 | -------------------------------------------------------------------------------- /config/captcha.php: -------------------------------------------------------------------------------- 1 | ['2', '3', '4', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'm', 'n', 'p', 'q', 'r', 't', 'u', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'M', 'N', 'P', 'Q', 'R', 'T', 'U', 'X', 'Y', 'Z'], 5 | 'default' => [ 6 | 'length' => 10, 7 | 'width' => 150, 8 | 'height' => 40, 9 | 'quality' => 90, 10 | 'lines' => 10, 11 | 'math' => true, 12 | ] 13 | ]; 14 | -------------------------------------------------------------------------------- /config/hashing.php: -------------------------------------------------------------------------------- 1 | 'bcrypt', 6 | 'bcrypt' => [ 7 | 'rounds' => env('BCRYPT_ROUNDS', 10), 8 | ], 9 | 'argon' => [ 10 | 'memory' => 1024, 11 | 'threads' => 2, 12 | 'time' => 2, 13 | ], 14 | ]; 15 | -------------------------------------------------------------------------------- /config/mail.php: -------------------------------------------------------------------------------- 1 | env('MAIL_DRIVER', 'smtp'), 7 | 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), 8 | 'port' => env('MAIL_PORT', 587), 9 | 'from' => [ 10 | 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), 11 | 'name' => env('MAIL_FROM_NAME', 'Example'), 12 | ], 13 | 'encryption' => env('MAIL_ENCRYPTION', 'tls'), 14 | 'username' => env('MAIL_USERNAME'), 15 | 'password' => env('MAIL_PASSWORD'), 16 | 'sendmail' => '/usr/sbin/sendmail -bs', 17 | 'markdown' => [ 18 | 'theme' => 'default', 19 | 'paths' => [ 20 | resource_path('views/vendor/mail'), 21 | ], 22 | ], 23 | 'log_channel' => env('MAIL_LOG_CHANNEL'), 24 | ]; 25 | -------------------------------------------------------------------------------- /config/queue.php: -------------------------------------------------------------------------------- 1 | env('QUEUE_CONNECTION', 'sync'), 6 | 'connections' => [ 7 | 'sync' => [ 8 | 'driver' => 'sync', 9 | ], 10 | 'database' => [ 11 | 'driver' => 'database', 12 | 'table' => 'jobs', 13 | 'queue' => 'default', 14 | 'retry_after' => 90, 15 | ], 16 | 'beanstalkd' => [ 17 | 'driver' => 'beanstalkd', 18 | 'host' => 'localhost', 19 | 'queue' => 'default', 20 | 'retry_after' => 90, 21 | 'block_for' => 0, 22 | ], 23 | 'sqs' => [ 24 | 'driver' => 'sqs', 25 | 'key' => env('AWS_ACCESS_KEY_ID'), 26 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 27 | 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), 28 | 'queue' => env('SQS_QUEUE', 'your-queue-name'), 29 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 30 | ], 31 | 'redis' => [ 32 | 'driver' => 'redis', 33 | 'connection' => 'default', 34 | 'queue' => env('REDIS_QUEUE', 'default'), 35 | 'retry_after' => 90, 36 | 'block_for' => null, 37 | ], 38 | ], 39 | 'failed' => [ 40 | 'driver' => env('QUEUE_FAILED_DRIVER', 'database'), 41 | 'database' => env('DB_CONNECTION', 'mysql'), 42 | 'table' => 'failed_jobs', 43 | ], 44 | ]; 45 | -------------------------------------------------------------------------------- /config/scout.php: -------------------------------------------------------------------------------- 1 | env('SCOUT_DRIVER', 'tntsearch'), 6 | 'prefix' => env('SCOUT_PREFIX', ''), 7 | 'queue' => env('SCOUT_QUEUE', false), 8 | 'chunk' => [ 9 | 'searchable' => 500, 10 | 'unsearchable' => 500, 11 | ], 12 | 'soft_delete' => false, 13 | 'algolia' => [ 14 | 'id' => env('ALGOLIA_APP_ID', ''), 15 | 'secret' => env('ALGOLIA_SECRET', ''), 16 | ], 17 | 'tntsearch' => [ 18 | 'storage' => storage_path(), 19 | 'fuzziness' => env('TNTSEARCH_FUZZINESS', true), 20 | 'fuzzy' => [ 21 | 'prefix_length' => 4, 22 | 'max_expansions' => 200, 23 | 'distance' => 2 24 | ], 25 | 'asYouType' => false, 26 | 'searchBoolean' => env('TNTSEARCH_BOOLEAN', true), 27 | ], 28 | ]; 29 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'token' => env('POSTMARK_TOKEN'), 6 | ], 7 | 'ses' => [ 8 | 'key' => env('AWS_ACCESS_KEY_ID'), 9 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 10 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 11 | ], 12 | 'stripe' => [ 13 | 'model' => App\User::class, 14 | 'key' => env('STRIPE_KEY'), 15 | 'secret' => env('STRIPE_SECRET'), 16 | 'webhook' => [ 17 | 'secret' => env('STRIPE_WEBHOOK_SECRET'), 18 | 'tolerance' => env('STRIPE_WEBHOOK_TOLERANCE', 300), 19 | ], 20 | ], 21 | ]; 22 | -------------------------------------------------------------------------------- /config/session.php: -------------------------------------------------------------------------------- 1 | env('SESSION_DRIVER', 'file'), 9 | 'lifetime' => env('SESSION_LIFETIME', 120), 10 | 'expire_on_close' => false, 11 | 'encrypt' => false, 12 | 'files' => storage_path('framework/sessions'), 13 | 'connection' => env('SESSION_CONNECTION', null), 14 | 'table' => 'sessions', 15 | 'store' => env('SESSION_STORE', null), 16 | 'lottery' => [2, 100], 17 | 'cookie' => env( 18 | 'SESSION_COOKIE', 19 | Str::slug(env('APP_NAME', 'laravel'), '_').'_session' 20 | ), 21 | 'path' => '/', 22 | 'domain' => env('SESSION_DOMAIN', null), 23 | 'secure' => env('SESSION_SECURE_COOKIE', false), 24 | 'http_only' => true, 25 | // Supported: "lax", "strict" 26 | 'same_site' => null, 27 | ]; 28 | -------------------------------------------------------------------------------- /config/view.php: -------------------------------------------------------------------------------- 1 | [ 5 | resource_path('views'), 6 | ], 7 | 'compiled' => env( 8 | 'VIEW_COMPILED_PATH', 9 | realpath(storage_path('framework/views')) 10 | ), 11 | ]; 12 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite 2 | *.sqlite-journal 3 | -------------------------------------------------------------------------------- /database/factories/AlbumFactory.php: -------------------------------------------------------------------------------- 1 | define(Album::class, function (Faker $faker) { 8 | return [ 9 | 'title' => $faker->sentence, 10 | 'content' => $faker->paragraphs(1, true), 11 | ]; 12 | }); 13 | -------------------------------------------------------------------------------- /database/factories/ChestFactory.php: -------------------------------------------------------------------------------- 1 | define(Chest::class, function (Faker $faker) { 8 | return [ 9 | 'title' => $faker->sentence, 10 | 'content' => [ 11 | [ 12 | 'type' => 'url', 13 | 'name' => 'URL Login', 14 | 'value' => $faker->url, 15 | ], 16 | [ 17 | 'type' => 'text', 18 | 'name' => 'Email', 19 | 'value' => $faker->email, 20 | ], 21 | [ 22 | 'type' => 'password', 23 | 'name' => 'Mot de passe', 24 | 'value' => $faker->password, 25 | ], 26 | ], 27 | ]; 28 | }); 29 | -------------------------------------------------------------------------------- /database/factories/LinkFactory.php: -------------------------------------------------------------------------------- 1 | define(Link::class, function (Faker $faker) { 9 | $is_watched = $faker->boolean(); 10 | $http_status_code = $faker->randomElement([ 11 | null, 200, 302, 404, 500, 12 | ]); 13 | 14 | return [ 15 | 'title' => $faker->sentence, 16 | 'content' => $faker->paragraph, 17 | 'url' => $faker->url, 18 | 'is_watched' => $is_watched, 19 | 'http_status' => $http_status_code, 20 | 'http_checked_at' => $http_status_code ? $faker->dateTimeThisMonth : null, 21 | ]; 22 | }); 23 | -------------------------------------------------------------------------------- /database/factories/PostFactory.php: -------------------------------------------------------------------------------- 1 | define(Post::class, function (Faker $faker) { 13 | return [ 14 | 'is_private' => 0, 15 | 'is_pinned' => 0, 16 | 'user_id' => 1, 17 | ]; 18 | }); 19 | 20 | $factory->state(Post::class, 'link', function (Faker $faker) { 21 | return ['postable_type' => Link::class,]; 22 | }); 23 | 24 | $factory->state(Post::class, 'story', function (Faker $faker) { 25 | return ['postable_type' => Story::class,]; 26 | }); 27 | 28 | $factory->state(Post::class, 'chest', function (Faker $faker) { 29 | return ['postable_type' => Chest::class,]; 30 | }); 31 | 32 | $factory->state(Post::class, 'album', function (Faker $faker) { 33 | return ['postable_type' => Album::class,]; 34 | }); 35 | 36 | $factory->state(Post::class, 'private', function (Faker $faker) { 37 | return [ 38 | 'is_private' => 1, 39 | ]; 40 | }); 41 | 42 | $factory->afterCreating(Post::class, function (Post $post, Faker $faker) { 43 | if ($post->postable_type == Link::class) { 44 | $post->postable_id = factory(Link::class)->create()->id; 45 | } else if ($post->postable_type == Story::class) { 46 | $post->postable_id = factory(Story::class)->create()->id; 47 | } else if ($post->postable_type == Chest::class) { 48 | $post->postable_id = factory(Chest::class)->create()->id; 49 | } else if ($post->postable_type == Album::class) { 50 | $post->postable_id = factory(Album::class)->create()->id; 51 | } 52 | $post->save(); 53 | }); 54 | -------------------------------------------------------------------------------- /database/factories/StoryFactory.php: -------------------------------------------------------------------------------- 1 | define(Story::class, function (Faker $faker) { 8 | return [ 9 | 'title' => $faker->sentence, 10 | 'slug' => $faker->unique->slug, 11 | 'content' => $faker->paragraphs(5, true), 12 | ]; 13 | }); 14 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | define(User::class, function (Faker $faker) { 9 | return [ 10 | 'name' => $faker->name, 11 | 'email' => $faker->unique()->safeEmail, 12 | 'email_verified_at' => now(), 13 | 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 14 | 'is_admin' => true, 15 | 'api_token' => User::generateApiToken(), 16 | 'remember_token' => Str::random(10), 17 | ]; 18 | }); 19 | -------------------------------------------------------------------------------- /database/factories/WallFactory.php: -------------------------------------------------------------------------------- 1 | define(Wall::class, function (Faker $faker) { 9 | $title = $faker->word; 10 | 11 | return [ 12 | 'title' => $title, 13 | 'slug' => Str::slug($title), 14 | 'restrict_tags' => [], 15 | 'restrict_cards' => [], 16 | 'appearance' => Wall::APPEARANCE_DEFAULT, 17 | 'is_default' => false, 18 | ]; 19 | }); 20 | -------------------------------------------------------------------------------- /database/migrations/2019_08_12_080000_create_users_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('name'); 19 | $table->string('email')->unique(); 20 | $table->timestamp('email_verified_at')->nullable(); 21 | $table->string('password'); 22 | $table->string('api_token')->unique()->index(); 23 | $table->rememberToken(); 24 | $table->timestamps(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists('users'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /database/migrations/2019_08_20_100000_create_password_resets_table.php: -------------------------------------------------------------------------------- 1 | string('email')->index(); 18 | $table->string('token'); 19 | $table->timestamp('created_at')->nullable(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::dropIfExists('password_resets'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2019_08_22_120000_create_posts_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 13 | $table->bigInteger('postable_id')->nullable()->unsigned()->index(); 14 | $table->string('postable_type')->nullable()->index(); 15 | $table->boolean('is_private')->default(false)->index(); 16 | $table->timestamps(); 17 | $table->index(['postable_id', 'postable_type']); 18 | }); 19 | } 20 | 21 | public function down() 22 | { 23 | Schema::dropIfExists('posts'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /database/migrations/2019_08_22_130000_create_links_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 13 | $table->string('title'); 14 | $table->text('content')->nullable(); 15 | $table->text('extra')->nullable(); 16 | $table->string('url')->nullable(); 17 | $table->timestamps(); 18 | }); 19 | } 20 | 21 | public function down() 22 | { 23 | Schema::dropIfExists('links'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /database/migrations/2019_08_22_140000_create_stories_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 13 | $table->string('title'); 14 | $table->string('slug')->unique(); 15 | $table->text('content')->nullable(); 16 | $table->timestamps(); 17 | }); 18 | } 19 | 20 | public function down() 21 | { 22 | Schema::dropIfExists('stories'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /database/migrations/2019_08_22_150000_create_tags_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 13 | $table->string('name')->unique(); 14 | $table->timestamps(); 15 | }); 16 | 17 | Schema::create('taggables', function (Blueprint $table) { 18 | $table->bigInteger('tag_id')->unsigned()->index(); 19 | $table->bigInteger('taggable_id')->unsigned()->index(); 20 | $table->string('taggable_type')->index(); 21 | $table->index(['taggable_id', 'taggable_type']); 22 | }); 23 | } 24 | 25 | public function down() 26 | { 27 | Schema::dropIfExists('tags'); 28 | Schema::dropIfExists('taggables'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/migrations/2019_08_22_160000_create_chests_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 13 | $table->string('title'); 14 | $table->text('content')->nullable(); 15 | $table->timestamps(); 16 | }); 17 | } 18 | 19 | public function down() 20 | { 21 | Schema::dropIfExists('chests'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /database/migrations/2019_09_02_110000_add_archive_columns_to_links_table.php: -------------------------------------------------------------------------------- 1 | string('archive')->nullable()->after('extra'); 13 | }); 14 | 15 | Schema::table('links', function (Blueprint $table) { 16 | $table->renameColumn('extra', 'preview'); 17 | }); 18 | } 19 | 20 | public function down() 21 | { 22 | Schema::table('links', function (Blueprint $table) { 23 | $table->renameColumn('preview', 'extra'); 24 | }); 25 | 26 | Schema::table('links', function (Blueprint $table) { 27 | $table->dropColumn('archive'); 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/migrations/2019_09_02_120000_create_jobs_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 13 | $table->string('queue')->index(); 14 | $table->longText('payload'); 15 | $table->unsignedTinyInteger('attempts'); 16 | $table->unsignedInteger('reserved_at')->nullable(); 17 | $table->unsignedInteger('available_at'); 18 | $table->unsignedInteger('created_at'); 19 | }); 20 | } 21 | 22 | public function down() 23 | { 24 | Schema::dropIfExists('jobs'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /database/migrations/2019_09_02_130000_create_failed_jobs_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 13 | $table->text('connection'); 14 | $table->text('queue'); 15 | $table->longText('payload'); 16 | $table->longText('exception'); 17 | $table->timestamp('failed_at')->useCurrent(); 18 | }); 19 | } 20 | 21 | public function down() 22 | { 23 | Schema::dropIfExists('failed_jobs'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /database/migrations/2019_09_25_211017_create_secure_logins_table.php: -------------------------------------------------------------------------------- 1 | string('token')->unique()->index(); 13 | $table->string('code')->unique()->index(); 14 | $table->bigInteger('user_id')->unsigned()->index(); 15 | $table->dateTime('expires_at')->index(); 16 | $table->timestamps(); 17 | }); 18 | } 19 | 20 | public function down() 21 | { 22 | Schema::dropIfExists('secure_logins'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /database/migrations/2019_09_26_153342_create_devices_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 13 | $table->bigInteger('user_id')->unsigned()->index(); 14 | $table->string('platform')->nullable(); 15 | $table->string('platform_version')->nullable(); 16 | $table->string('browser')->nullable(); 17 | $table->string('browser_version')->nullable(); 18 | $table->boolean('is_desktop')->default(0); 19 | $table->boolean('is_mobile')->default(0); 20 | $table->string('language')->nullable(); 21 | $table->boolean('is_trusted')->default(0)->index(); 22 | $table->boolean('is_untrusted')->default(0)->index(); 23 | $table->timestamps(); 24 | 25 | // $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); 26 | }); 27 | } 28 | 29 | public function down() 30 | { 31 | Schema::dropIfExists('devices'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /database/migrations/2019_09_26_153342_create_logins_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 13 | $table->ipAddress('ip_address'); 14 | $table->string('type')->default(\Lab404\AuthChecker\Models\Login::TYPE_LOGIN)->index(); 15 | $table->bigInteger('user_id')->unsigned()->index(); 16 | $table->bigInteger('device_id')->unsigned()->index()->nullable(); 17 | $table->timestamps(); 18 | 19 | // $table->foreign('device_id')->references('id')->on('devices')->onDelete('cascade'); 20 | }); 21 | } 22 | 23 | public function down() 24 | { 25 | Schema::dropIfExists('logins'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /database/migrations/2019_10_07_140000_add_user_to_posts_table.php: -------------------------------------------------------------------------------- 1 | bigInteger('user_id')->unsigned()->index()->nullable()->after('is_private'); 13 | }); 14 | } 15 | 16 | public function down() 17 | { 18 | Schema::table('posts', function (Blueprint $table) { 19 | $table->dropColumn('user_id'); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /database/migrations/2019_10_10_110000_add_is_admin_to_users_table.php: -------------------------------------------------------------------------------- 1 | boolean('is_admin')->index()->default(false)->after('name'); 13 | }); 14 | } 15 | 16 | public function down() 17 | { 18 | Schema::table('users', function (Blueprint $table) { 19 | $table->dropColumn('is_admin'); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /database/migrations/2019_10_21_140000_create_shares_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 13 | $table->string('token')->index(); 14 | $table->bigInteger('post_id')->unsigned()->index(); 15 | $table->dateTime('expires_at'); 16 | $table->timestamps(); 17 | 18 | $table->foreign('post_id')->references('id')->on('posts')->onDelete('cascade'); 19 | }); 20 | } 21 | 22 | public function down() 23 | { 24 | Schema::table('shares', function (Blueprint $table) { 25 | $table->dropIndex(['post_id']); 26 | }); 27 | 28 | Schema::dropIfExists('shares'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/migrations/2019_10_22_190000_add_is_pinned_to_posts_table.php: -------------------------------------------------------------------------------- 1 | boolean('is_pinned')->index()->default(false)->after('is_private'); 13 | }); 14 | } 15 | 16 | public function down() 17 | { 18 | Schema::table('posts', function (Blueprint $table) { 19 | $table->dropColumn('is_pinned'); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /database/migrations/2019_11_02_190000_create_media_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 13 | $table->morphs('model'); 14 | $table->string('collection_name'); 15 | $table->string('name'); 16 | $table->string('file_name'); 17 | $table->string('mime_type')->nullable(); 18 | $table->string('disk'); 19 | $table->unsignedBigInteger('size'); 20 | $table->json('manipulations'); 21 | $table->json('custom_properties'); 22 | $table->json('responsive_images'); 23 | $table->unsignedInteger('order_column')->nullable(); 24 | $table->nullableTimestamps(); 25 | }); 26 | } 27 | 28 | public function down() 29 | { 30 | Schema::dropIfExists('media'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2019_11_02_200000_create_albums_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 13 | $table->string('title'); 14 | $table->text('content')->nullable(); 15 | $table->timestamps(); 16 | }); 17 | } 18 | 19 | public function down() 20 | { 21 | Schema::dropIfExists('albums'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /database/migrations/2019_11_20_130000_create_comments_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 13 | $table->bigInteger('comment_id')->nullable()->index(); 14 | $table->bigInteger('post_id')->unsigned()->index(); 15 | $table->text('content'); 16 | $table->bigInteger('user_id')->unsigned()->nullable()->index(); 17 | $table->string('user_name')->nullable(); 18 | $table->string('user_email')->nullable()->index(); 19 | $table->boolean('is_visible')->default(false)->index(); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | public function down() 25 | { 26 | Schema::dropIfExists('comments'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /database/migrations/2020_01_03_150000_create_walls_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 13 | $table->string('title'); 14 | $table->string('slug')->unique(); 15 | $table->text('restrict_tags')->nullable(); 16 | $table->text('restrict_cards')->nullable(); 17 | $table->text('appearance')->nullable(); 18 | $table->bigInteger('user_id')->unsigned()->index()->nullable(); 19 | $table->boolean('is_private')->default(false)->index(); 20 | $table->boolean('is_default')->default(false)->index(); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | public function down() 26 | { 27 | Schema::dropIfExists('walls'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /database/migrations/2020_01_03_170000_update_logins_and_devices_table_user_relation.php: -------------------------------------------------------------------------------- 1 | string('user_type')->after('user_id')->nullable(); 13 | }); 14 | 15 | Schema::table('devices', function (Blueprint $table) { 16 | $table->string('user_type')->after('user_id')->nullable(); 17 | }); 18 | } 19 | 20 | public function down() 21 | { 22 | Schema::table('logins', function (Blueprint $table) { 23 | $table->dropColumn('user_type'); 24 | }); 25 | 26 | Schema::table('devices', function (Blueprint $table) { 27 | $table->dropColumn('user_type'); 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/migrations/2020_06_17_060000_add_health_checks_to_links_table.php: -------------------------------------------------------------------------------- 1 | boolean('is_watched')->after('url')->default(true); 13 | $table->unsignedSmallInteger('http_status')->after('is_watched')->nullable(); 14 | $table->timestamp('http_checked_at')->after('http_status')->nullable(); 15 | 16 | $table->index('http_status'); 17 | }); 18 | } 19 | 20 | public function down() 21 | { 22 | Schema::table('links', function (Blueprint $table) { 23 | $table->dropColumn([ 24 | 'is_watched', 25 | 'http_status', 26 | 'http_checked_at', 27 | ]); 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/seeds/LinkSeeder.php: -------------------------------------------------------------------------------- 1 | create(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.2' 2 | services: 3 | shaark: 4 | image: shaark 5 | build: . 6 | container_name: shaark 7 | restart: unless-stopped 8 | # volumes: 9 | # - "./env:/app/.env" 10 | # ports: 11 | # - "80:80/tcp" 12 | networks: 13 | - shaark 14 | - nginx_net 15 | logging: 16 | driver: "json-file" 17 | options: 18 | max-size: "10m" 19 | max-file: "5" 20 | 21 | mariadb: 22 | image: mariadb 23 | container_name: mariadb_shaark 24 | restart: unless-stopped 25 | volumes: 26 | - /opt/shaark/mariadb:/var/lib/mysql 27 | networks: 28 | - shaark 29 | environment: 30 | MYSQL_ROOT_PASSWORD: rootpassword9867ow3q459087w980 31 | MYSQL_PASSWORD: secret 32 | MYSQL_USER: homestead 33 | MYSQL_DATABASE: homestead 34 | logging: 35 | driver: "json-file" 36 | options: 37 | max-size: "10m" 38 | max-file: "5" 39 | 40 | 41 | redis: 42 | image: redis 43 | container_name: redis_shaark 44 | restart: unless-stopped 45 | volumes: 46 | - /opt/shaark/redis:/data 47 | networks: 48 | - shaark 49 | logging: 50 | driver: "json-file" 51 | options: 52 | max-size: "10m" 53 | max-file: "5" 54 | 55 | 56 | networks: 57 | shaark: 58 | name: shaark_net 59 | nginx_net: 60 | name: nginx_net 61 | -------------------------------------------------------------------------------- /documentation/archiving.md: -------------------------------------------------------------------------------- 1 | # Shaark - Archiving 2 | 3 | - [PDF Archiving](#pdf-archiving) 4 | - [Media Archiving](#media-archiving) 5 | - [Caveats](#caveats) 6 | 7 | ## PDF Archiving 8 | 9 | PDF archiving is used to backup your links as a PDF using printing functionnality of Chromium. 10 | We use [Puppeteer](https://github.com/GoogleChrome/puppeteer) to achieve it. 11 | 12 | ### Installation 13 | 14 | Puppeteer is installed as a Composer dependencies but needs Node dependencies that can be installed with: 15 | `npm install @nesk/puphpeteer --no-save` 16 | 17 | ### Configuration 18 | 19 | Once your app is installed, access the **Settings** section, enable **PDF archiving** then configure your **Node path** (defaults to `/usr/bin/node`). 20 | You can use the **Check** button to test your configuration. 21 | 22 | ## Media Archiving 23 | 24 | Media archiving is used to get original media file from Youtube, Soundcloud and [many more](http://ytdl-org.github.io/youtube-dl/supportedsites.html) using [youtube-dl](https://github.com/ytdl-org/youtube-dl/). 25 | 26 | ### Installation 27 | 28 | You need to install manually [youtube-dl](https://github.com/ytdl-org/youtube-dl/#installation) and have access to Python on your system. 29 | 30 | ### Configuration 31 | 32 | Once your app is installed, access the **Settings** section, enable **Media archiving** then configure your **Youtube-dl path** (defaults to `/usr/bin/youtube-dl`) and your **Python path** (defaults to `/usr/bin/python`). 33 | You can use the **Check** button to test your configuration. 34 | 35 | ## Caveats 36 | 37 | Archiving is performance expensive and should be run in production using queues. See our [installation configuration](https://github.com/MarceauKa/shaark/blob/dev/documentation/installation.md) to learn more about it. 38 | -------------------------------------------------------------------------------- /documentation/backup.md: -------------------------------------------------------------------------------- 1 | # Shaark - Backup 2 | 3 | ## Getting started 4 | 5 | ⚠️ Make sure you have configured a CRON job. 6 | 7 | `* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1` 8 | 9 | ## Configuration 10 | 11 | Open your `.env` file and configure `BACKUP_*` lines. 12 | 13 | Available driver are `local` and `ftp`. If you choose `local` you're done with the configuration. 14 | All others lines are for the `ftp` driver. 15 | 16 | ## Settings 17 | 18 | In the settings section of the app, go to the **Backup** panel. You can: 19 | 20 | - Enable or disable automatic backup. 21 | - Choose to backup **daily** or **weekly**. 22 | - Choose to save only the database. Saving files can consume a lot of disk space, especially if you use [archiving](https://github.com/MarceauKa/shaark/blob/dev/documentation/archiving.md). 23 | 24 | ## Commands 25 | 26 | - Run backup manually: `php artisan backup:run` 27 | - Run backup manually (only db): `php artisan backup:run --only-db` 28 | - List all backups: `php artisan backup:list` 29 | - Clean old backups: `php artisan backup:clean` 30 | -------------------------------------------------------------------------------- /documentation/comments.md: -------------------------------------------------------------------------------- 1 | # Shaark - Comments 2 | 3 | Links, stories, chests and albums can have comments. Comments are **disabled** by default. 4 | Enable them in the settings section. 5 | 6 | ## Configuration 7 | 8 | - **Comments enabled**: Yes or no 9 | 10 | - **Guests can see comments**: Yes or no 11 | 12 | - **Guests can add comments**: Yes or no 13 | 14 | - **New comment moderation**: 15 | - Disabled 16 | - White-listing (see [moderation](#moderation)) 17 | - All 18 | 19 | ⚠️ The **All** option below doesn't take into account comments from logged users. 20 | 21 | - **New comment notification**: 22 | - Disabled 23 | - White-listing (see [moderation](#moderation)) 24 | - All 25 | 26 | ⚠️ The **All** option below includes comments from logged users. 27 | 28 | ## Moderation 29 | 30 | Guests are asked to give their name and email address when commenting. Their **email are only visible to you**. 31 | When you have moderated a new comment from a guest (white-listing), their future comments will be **automatically validated** if the guest use the same email address. 32 | -------------------------------------------------------------------------------- /documentation/docker.md: -------------------------------------------------------------------------------- 1 | # Shaark - Docker 2 | 3 | Docker can be used to deploy Shaark. By default it will install Shaark in production mode. 4 | This behaviour can be changed by editing the Dockerfile. 5 | 6 | ## Installation 7 | 8 | ### Requirements 9 | 10 | **NOTE** : You may (probably) need to customize the dockerfile (e.g. changing the network the container is attached at) to suit your case. 11 | 12 | ### Quick Run 13 | 14 | You can try the application in minutes by running those commands: 15 | 16 | ``` 17 | git clone https://github.com/MarceauKa/shaark 18 | cd shaark 19 | docker-compose up -d . 20 | ``` 21 | 22 | And then running a reverse proxy like NGINX on the `nginx_net`. 23 | If you don't already have a reverse proxy, you can google on how to create one. 24 | -------------------------------------------------------------------------------- /phpunit.dusk.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./tests/Browser/AuthTest.php 14 | ./tests/Browser/BrowseTest.php 15 | ./tests/Browser/LinkFormTest.php 16 | ./tests/Browser/TempSharingTest.php 17 | 18 | 19 | 20 | 21 | ./app 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Redirect Trailing Slashes If Not A Folder... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_URI} (.+)/$ 15 | RewriteRule ^ %1 [L,R=301] 16 | 17 | # Handle Front Controller... 18 | RewriteCond %{REQUEST_FILENAME} !-d 19 | RewriteCond %{REQUEST_FILENAME} !-f 20 | RewriteRule ^ index.php [L] 21 | 22 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceauKa/shaark/2be59cc231dca58a02ea65d314d95ebbff4f6c42/public/favicon.ico -------------------------------------------------------------------------------- /public/fonts/IBMPlexSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceauKa/shaark/2be59cc231dca58a02ea65d314d95ebbff4f6c42/public/fonts/IBMPlexSans-Bold.ttf -------------------------------------------------------------------------------- /public/fonts/IBMPlexSans-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceauKa/shaark/2be59cc231dca58a02ea65d314d95ebbff4f6c42/public/fonts/IBMPlexSans-Bold.woff -------------------------------------------------------------------------------- /public/fonts/IBMPlexSans-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceauKa/shaark/2be59cc231dca58a02ea65d314d95ebbff4f6c42/public/fonts/IBMPlexSans-Bold.woff2 -------------------------------------------------------------------------------- /public/fonts/IBMPlexSans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceauKa/shaark/2be59cc231dca58a02ea65d314d95ebbff4f6c42/public/fonts/IBMPlexSans.ttf -------------------------------------------------------------------------------- /public/fonts/IBMPlexSans.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceauKa/shaark/2be59cc231dca58a02ea65d314d95ebbff4f6c42/public/fonts/IBMPlexSans.woff -------------------------------------------------------------------------------- /public/fonts/IBMPlexSans.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceauKa/shaark/2be59cc231dca58a02ea65d314d95ebbff4f6c42/public/fonts/IBMPlexSans.woff2 -------------------------------------------------------------------------------- /public/fonts/vendor/@fortawesome/fontawesome-free/webfa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceauKa/shaark/2be59cc231dca58a02ea65d314d95ebbff4f6c42/public/fonts/vendor/@fortawesome/fontawesome-free/webfa-solid-900.eot -------------------------------------------------------------------------------- /public/fonts/vendor/@fortawesome/fontawesome-free/webfa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceauKa/shaark/2be59cc231dca58a02ea65d314d95ebbff4f6c42/public/fonts/vendor/@fortawesome/fontawesome-free/webfa-solid-900.ttf -------------------------------------------------------------------------------- /public/fonts/vendor/@fortawesome/fontawesome-free/webfa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceauKa/shaark/2be59cc231dca58a02ea65d314d95ebbff4f6c42/public/fonts/vendor/@fortawesome/fontawesome-free/webfa-solid-900.woff -------------------------------------------------------------------------------- /public/fonts/vendor/@fortawesome/fontawesome-free/webfa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceauKa/shaark/2be59cc231dca58a02ea65d314d95ebbff4f6c42/public/fonts/vendor/@fortawesome/fontawesome-free/webfa-solid-900.woff2 -------------------------------------------------------------------------------- /public/fonts/vendor/mavon-editor/dist/fontello.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceauKa/shaark/2be59cc231dca58a02ea65d314d95ebbff4f6c42/public/fonts/vendor/mavon-editor/dist/fontello.eot -------------------------------------------------------------------------------- /public/fonts/vendor/mavon-editor/dist/fontello.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceauKa/shaark/2be59cc231dca58a02ea65d314d95ebbff4f6c42/public/fonts/vendor/mavon-editor/dist/fontello.ttf -------------------------------------------------------------------------------- /public/fonts/vendor/mavon-editor/dist/fontello.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceauKa/shaark/2be59cc231dca58a02ea65d314d95ebbff4f6c42/public/fonts/vendor/mavon-editor/dist/fontello.woff -------------------------------------------------------------------------------- /public/fonts/vendor/mavon-editor/dist/fontello.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceauKa/shaark/2be59cc231dca58a02ea65d314d95ebbff4f6c42/public/fonts/vendor/mavon-editor/dist/fontello.woff2 -------------------------------------------------------------------------------- /public/images/logo-shaark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceauKa/shaark/2be59cc231dca58a02ea65d314d95ebbff4f6c42/public/images/logo-shaark.png -------------------------------------------------------------------------------- /public/images/logo-shaarli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceauKa/shaark/2be59cc231dca58a02ea65d314d95ebbff4f6c42/public/images/logo-shaarli.png -------------------------------------------------------------------------------- /public/images/vendor/tui-editor/dist/tui-editor-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceauKa/shaark/2be59cc231dca58a02ea65d314d95ebbff4f6c42/public/images/vendor/tui-editor/dist/tui-editor-2x.png -------------------------------------------------------------------------------- /public/images/vendor/tui-editor/dist/tui-editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceauKa/shaark/2be59cc231dca58a02ea65d314d95ebbff4f6c42/public/images/vendor/tui-editor/dist/tui-editor.png -------------------------------------------------------------------------------- /public/js/manifest.js: -------------------------------------------------------------------------------- 1 | !function(e){function r(r){for(var n,l,f=r[0],i=r[1],a=r[2],c=0,s=[];c 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /resources/js/components/Confirm.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 67 | -------------------------------------------------------------------------------- /resources/js/components/Flash.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 28 | -------------------------------------------------------------------------------- /resources/js/components/Loader.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | -------------------------------------------------------------------------------- /resources/js/components/PreventBrowserUnload.vue: -------------------------------------------------------------------------------- 1 | 22 | -------------------------------------------------------------------------------- /resources/js/components/PurgeLogins.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 31 | -------------------------------------------------------------------------------- /resources/js/mixins/audit.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | data: function () { 3 | return { 4 | hasChanged: false, 5 | auditForm: null, 6 | } 7 | }, 8 | 9 | methods: { 10 | startAudit() { 11 | this.clearAudit(); 12 | this.registerUnloadingPrevention(); 13 | this.auditForm = JSON.parse(JSON.stringify(this.form)); 14 | }, 15 | 16 | clearAudit() { 17 | this.hasChanged = false; 18 | this.auditForm = null; 19 | }, 20 | 21 | registerUnloadingPrevention() { 22 | window.addEventListener('beforeunload', (event) => { 23 | if (this.hasChanged === true) { 24 | event.stopPropagation(); 25 | event.preventDefault(); 26 | event.returnValue = ''; 27 | } 28 | }); 29 | } 30 | }, 31 | 32 | watch: { 33 | 'form': { 34 | handler: function (value, old) { 35 | let changed = false; 36 | const hasChanged = (newValue, originalValue) => { 37 | if (typeof newValue === 'object') { 38 | return JSON.stringify(newValue) !== JSON.stringify(originalValue); 39 | } 40 | 41 | return newValue !== originalValue; 42 | }; 43 | 44 | Object.keys(value).forEach((key) => { 45 | if (hasChanged(value[key], this.auditForm[key])) { 46 | changed = true; 47 | } 48 | }); 49 | 50 | this.hasChanged = changed; 51 | }, 52 | deep: true 53 | } 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /resources/js/mixins/auth.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | methods: { 3 | isLogged() { 4 | return this.user() !== null; 5 | }, 6 | 7 | user() { 8 | return window.user || null; 9 | }, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /resources/js/mixins/bus.js: -------------------------------------------------------------------------------- 1 | function VueBus(Vue) { 2 | const bus = new Vue(); 3 | 4 | Object.defineProperties(bus, { 5 | on: { 6 | get() { 7 | return this.$on.bind(this); 8 | } 9 | }, 10 | once: { 11 | get() { 12 | return this.$once.bind(this); 13 | } 14 | }, 15 | off: { 16 | get() { 17 | return this.$off.bind(this); 18 | } 19 | }, 20 | emit: { 21 | get() { 22 | return this.$emit.bind(this); 23 | } 24 | } 25 | }); 26 | 27 | Object.defineProperty(Vue, 'bus', { 28 | get() { 29 | return bus; 30 | } 31 | }); 32 | 33 | Object.defineProperty(Vue.prototype, '$bus', { 34 | get() { 35 | return bus; 36 | } 37 | }); 38 | } 39 | 40 | if (typeof window !== 'undefined' && window.Vue) { 41 | window.Vue.use(VueBus); 42 | } 43 | 44 | export default VueBus; 45 | -------------------------------------------------------------------------------- /resources/js/mixins/copyToClipboard.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | methods: { 3 | copyToClipboard($event, value) { 4 | let original = $event.target.innerHTML; 5 | $event.target.innerHTML = this.__('Copied'); 6 | 7 | let timeout = setTimeout(() => { 8 | $event.target.innerHTML = original; 9 | }, 1000); 10 | 11 | const el = document.createElement('textarea'); 12 | let storeContentEditable = el.contentEditable; 13 | let storeReadOnly = el.readOnly; 14 | 15 | el.value = value; 16 | el.contentEditable = true; 17 | el.readOnly = false; 18 | el.setAttribute('readonly', false); // Make it readonly false for iOS compatability 19 | el.setAttribute('contenteditable', true); // Make it editable for iOS 20 | el.style.position = 'absolute'; 21 | el.style.left = '-9999px'; 22 | document.body.appendChild(el); 23 | 24 | const selected = document.getSelection().rangeCount > 0 ? document.getSelection().getRangeAt(0) : false; 25 | 26 | el.select(); 27 | el.setSelectionRange(0, 999999); 28 | document.execCommand('copy'); 29 | document.body.removeChild(el); 30 | 31 | if (selected) { 32 | document.getSelection().removeAllRanges(); 33 | document.getSelection().addRange(selected); 34 | } 35 | 36 | el.contentEditable = storeContentEditable; 37 | el.readOnly = storeReadOnly; 38 | }, 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /resources/js/mixins/formErrors.js: -------------------------------------------------------------------------------- 1 | const formErrors = { 2 | data: function () { 3 | return { 4 | formErrors: {}, 5 | } 6 | }, 7 | 8 | methods: { 9 | setFormError(error) { 10 | this.formErrors = error.response.data.errors; 11 | }, 12 | 13 | hasFormError(key) { 14 | return this.formErrors.hasOwnProperty(key); 15 | }, 16 | 17 | firstFormError(key) { 18 | return this.hasFormError(key) ? this.formErrors[key][0] : null; 19 | }, 20 | 21 | resetFormError() { 22 | this.formErrors = {}; 23 | } 24 | } 25 | }; 26 | 27 | export default formErrors; 28 | -------------------------------------------------------------------------------- /resources/js/mixins/httpErrors.js: -------------------------------------------------------------------------------- 1 | const httpErrors = { 2 | data: function () { 3 | return { 4 | httpError: {}, 5 | } 6 | }, 7 | 8 | methods: { 9 | setHttpError(error) { 10 | this.httpError = error.response.data.message || null; 11 | }, 12 | 13 | hasHttpError(key) { 14 | return this.httpError; 15 | }, 16 | 17 | getHttpError(defaultMessage = null) { 18 | return this.hasHttpError() ? this.httpError : defaultMessage; 19 | }, 20 | 21 | toastHttpError(defaultMessage = null) { 22 | let message = this.getHttpError(defaultMessage); 23 | this.$toasted.error(message); 24 | } 25 | } 26 | }; 27 | 28 | export default httpErrors; 29 | -------------------------------------------------------------------------------- /resources/js/mixins/i18n.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | methods: { 3 | __(key, replace) { 4 | let i18n = window.i18n[key] ? window.i18n[key] : key; 5 | _.forEach(replace, (value, key) => { 6 | i18n = i18n.replace(':' + key, value) 7 | }); 8 | return i18n; 9 | }, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /resources/lang/de/auth.php: -------------------------------------------------------------------------------- 1 | 'Diese Kombination aus Zugangsdaten wurde nicht in unserer Datenbank gefunden.', 16 | 'throttle' => 'Zu viele Loginversuche. Versuchen Sie es bitte in :seconds Sekunden nochmal.', 17 | ]; 18 | -------------------------------------------------------------------------------- /resources/lang/de/pagination.php: -------------------------------------------------------------------------------- 1 | '« Zurück', 16 | 'next' => 'Weiter »', 17 | ]; 18 | -------------------------------------------------------------------------------- /resources/lang/de/passwords.php: -------------------------------------------------------------------------------- 1 | 'Passwörter müssen mindestens 8 Zeichen lang sein und korrekt bestätigt werden.', 16 | 'reset' => 'Das Passwort wurde zurückgesetzt!', 17 | 'sent' => 'Passworterinnerung wurde gesendet!', 18 | 'token' => 'Der Passwort-Wiederherstellungs-Schlüssel ist ungültig oder abgelaufen.', 19 | 'user' => 'Es konnte leider kein Nutzer mit dieser E-Mail-Adresse gefunden werden.', 20 | ]; 21 | -------------------------------------------------------------------------------- /resources/lang/en/auth.php: -------------------------------------------------------------------------------- 1 | 'These credentials do not match our records.', 17 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /resources/lang/en/pagination.php: -------------------------------------------------------------------------------- 1 | '« Previous', 17 | 'next' => 'Next »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /resources/lang/en/passwords.php: -------------------------------------------------------------------------------- 1 | 'Passwords must be at least eight characters and match the confirmation.', 17 | 'reset' => 'Your password has been reset!', 18 | 'sent' => 'We have e-mailed your password reset link!', 19 | 'token' => 'This password reset token is invalid.', 20 | 'user' => "We can't find a user with that e-mail address.", 21 | 22 | ]; 23 | -------------------------------------------------------------------------------- /resources/lang/fr/auth.php: -------------------------------------------------------------------------------- 1 | 'Ces identifiants ne correspondent pas à nos enregistrements', 16 | 'throttle' => 'Tentatives de connexion trop nombreuses. Veuillez essayer de nouveau dans :seconds secondes.', 17 | ]; 18 | -------------------------------------------------------------------------------- /resources/lang/fr/pagination.php: -------------------------------------------------------------------------------- 1 | '« Précédent', 16 | 'next' => 'Suivant »', 17 | ]; 18 | -------------------------------------------------------------------------------- /resources/lang/fr/passwords.php: -------------------------------------------------------------------------------- 1 | 'Les mots de passe doivent contenir au moins huit caractères et être identiques.', 16 | 'reset' => 'Votre mot de passe a été réinitialisé !', 17 | 'sent' => 'Nous vous avons envoyé par email le lien de réinitialisation du mot de passe !', 18 | 'token' => "Ce jeton de réinitialisation du mot de passe n'est pas valide.", 19 | 'user' => "Aucun utilisateur n'a été trouvé avec cette adresse email.", 20 | ]; 21 | -------------------------------------------------------------------------------- /resources/lang/ja/auth.php: -------------------------------------------------------------------------------- 1 | '認証情報と一致するレコードがありません。', 16 | 'throttle' => 'ログインの試行回数が多すぎます。:seconds 秒後にお試しください。', 17 | ]; 18 | -------------------------------------------------------------------------------- /resources/lang/ja/pagination.php: -------------------------------------------------------------------------------- 1 | '« 前', 16 | 'next' => '次 »', 17 | ]; 18 | -------------------------------------------------------------------------------- /resources/lang/ja/passwords.php: -------------------------------------------------------------------------------- 1 | 'パスワードは6文字以上かつ確認フィールドと一致していなければなりません。', 16 | 'reset' => 'パスワードをリセットしました。', 17 | 'sent' => 'パスワードリマインダーを送信しました。', 18 | 'token' => 'このパスワードリセットトークンは無効です。', 19 | 'user' => 'このメールアドレスに一致するユーザーを見つけることが出来ませんでした。', 20 | ]; 21 | -------------------------------------------------------------------------------- /resources/lang/nl/auth.php: -------------------------------------------------------------------------------- 1 | 'Deze combinatie van e-mailadres en wachtwoord is niet geldig.', 17 | 'throttle' => 'Te veel mislukte loginpogingen. Probeer het over :seconds seconden nogmaals.', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /resources/lang/nl/pagination.php: -------------------------------------------------------------------------------- 1 | '« Vorige', 17 | 'next' => 'Volgende »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /resources/lang/nl/passwords.php: -------------------------------------------------------------------------------- 1 | 'Het wachtwoord van uw account is gewijzigd.', 17 | 'sent' => 'We hebben een e-mail verstuurd met instructies om een nieuw wachtwoord in te stellen.', 18 | 'throttled' => 'Gelieve even te wachten voor u het opnieuw probeert.', 19 | 'token' => 'Dit wachtwoordhersteltoken is niet geldig.', 20 | 'user' => 'Geen gebruiker bekend met het e-mailadres.', 21 | 22 | ]; 23 | -------------------------------------------------------------------------------- /resources/lang/vendor/backup/zh-CN/notifications.php: -------------------------------------------------------------------------------- 1 | '异常信息: :message', 5 | 'exception_trace' => '异常跟踪: :trace', 6 | 'exception_message_title' => '异常信息', 7 | 'exception_trace_title' => '异常跟踪', 8 | 9 | 'backup_failed_subject' => ':application_name 备份失败', 10 | 'backup_failed_body' => '重要说明:备份 :application_name 时发生错误', 11 | 12 | 'backup_successful_subject' => ':application_name 备份成功', 13 | 'backup_successful_subject_title' => '备份成功!', 14 | 'backup_successful_body' => '好消息, :application_name 备份成功,位于磁盘 :disk_name 中。', 15 | 16 | 'cleanup_failed_subject' => '清除 :application_name 的备份失败。', 17 | 'cleanup_failed_body' => '清除备份 :application_name 时发生错误', 18 | 19 | 'cleanup_successful_subject' => '成功清除 :application_name 的备份', 20 | 'cleanup_successful_subject_title' => '成功清除备份!', 21 | 'cleanup_successful_body' => '成功清除 :disk_name 磁盘上 :application_name 的备份。', 22 | 23 | 'healthy_backup_found_subject' => ':disk_name 磁盘上 :application_name 的备份是健康的', 24 | 'healthy_backup_found_subject_title' => ':application_name 的备份是健康的', 25 | 'healthy_backup_found_body' => ':application_name 的备份是健康的。干的好!', 26 | 27 | 'unhealthy_backup_found_subject' => '重要说明::application_name 的备份不健康', 28 | 'unhealthy_backup_found_subject_title' => '重要说明::application_name 备份不健康。 :problem', 29 | 'unhealthy_backup_found_body' => ':disk_name 磁盘上 :application_name 的备份不健康。', 30 | 'unhealthy_backup_found_not_reachable' => '无法访问备份目标。 :error', 31 | 'unhealthy_backup_found_empty' => '根本没有此应用程序的备份。', 32 | 'unhealthy_backup_found_old' => '最近的备份创建于 :date ,太旧了。', 33 | 'unhealthy_backup_found_unknown' => '对不起,确切原因无法确定。', 34 | 'unhealthy_backup_found_full' => '备份占用了太多存储空间。当前占用了 :disk_usage ,高于允许的限制 :disk_limit。', 35 | ]; 36 | -------------------------------------------------------------------------------- /resources/lang/vendor/backup/zh-TW/notifications.php: -------------------------------------------------------------------------------- 1 | '異常訊息: :message', 5 | 'exception_trace' => '異常追蹤: :trace', 6 | 'exception_message_title' => '異常訊息', 7 | 'exception_trace_title' => '異常追蹤', 8 | 9 | 'backup_failed_subject' => ':application_name 備份失敗', 10 | 'backup_failed_body' => '重要說明:備份 :application_name 時發生錯誤', 11 | 12 | 'backup_successful_subject' => ':application_name 備份成功', 13 | 'backup_successful_subject_title' => '備份成功!', 14 | 'backup_successful_body' => '好消息, :application_name 備份成功,位於磁盤 :disk_name 中。', 15 | 16 | 'cleanup_failed_subject' => '清除 :application_name 的備份失敗。', 17 | 'cleanup_failed_body' => '清除備份 :application_name 時發生錯誤', 18 | 19 | 'cleanup_successful_subject' => '成功清除 :application_name 的備份', 20 | 'cleanup_successful_subject_title' => '成功清除備份!', 21 | 'cleanup_successful_body' => '成功清除 :disk_name 磁盤上 :application_name 的備份。', 22 | 23 | 'healthy_backup_found_subject' => ':disk_name 磁盤上 :application_name 的備份是健康的', 24 | 'healthy_backup_found_subject_title' => ':application_name 的備份是健康的', 25 | 'healthy_backup_found_body' => ':application_name 的備份是健康的。幹的好!', 26 | 27 | 'unhealthy_backup_found_subject' => '重要說明::application_name 的備份不健康', 28 | 'unhealthy_backup_found_subject_title' => '重要說明::application_name 備份不健康。 :problem', 29 | 'unhealthy_backup_found_body' => ':disk_name 磁盤上 :application_name 的備份不健康。', 30 | 'unhealthy_backup_found_not_reachable' => '無法訪問備份目標。 :error', 31 | 'unhealthy_backup_found_empty' => '根本沒有此應用程序的備份。', 32 | 'unhealthy_backup_found_old' => '最近的備份創建於 :date ,太舊了。', 33 | 'unhealthy_backup_found_unknown' => '對不起,確切原因無法確定。', 34 | 'unhealthy_backup_found_full' => '備份佔用了太多存儲空間。當前佔用了 :disk_usage ,高於允許的限制 :disk_limit。', 35 | ]; 36 | -------------------------------------------------------------------------------- /resources/sass/_variables.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'IBM Plex Sans'; 3 | src: url('/fonts/IBMPlexSans-Bold.woff2') format('woff2'), 4 | url('/fonts/IBMPlexSans-Bold.woff') format('woff'), 5 | url('/fonts/IBMPlexSans-Bold.ttf') format('truetype'); 6 | font-weight: bold; 7 | font-style: normal; 8 | } 9 | 10 | @font-face { 11 | font-family: 'IBM Plex Sans'; 12 | src: url('/fonts/IBMPlexSans.woff2') format('woff2'), 13 | url('/fonts/IBMPlexSans.woff') format('woff'), 14 | url('/fonts/IBMPlexSans.ttf') format('truetype'); 15 | font-weight: normal; 16 | font-style: normal; 17 | } 18 | 19 | $blue: #3490dc; 20 | $indigo: #6574cd; 21 | $purple: #9561e2; 22 | $pink: #f66d9b; 23 | $red: #e3342f; 24 | $orange: #f6993f; 25 | $yellow: #ffed4a; 26 | $green: #38c172; 27 | $teal: #4dc0b5; 28 | $cyan: #6cb2eb; 29 | $body-bg: #f8fafc; 30 | $font-family-sans-serif: 'IBM Plex Sans', Helvetica, Arial, Sans-Serif; 31 | $font-size-base: 1rem; 32 | $line-height-base: 1.6; 33 | $link-color: $blue; 34 | $link-hover-color: $indigo; 35 | $border-radius: 1rem; 36 | $box-shadow-sm: 0 10px 20px rgba(0, 0, 0, .1); 37 | $box-shadow: 0 10px 20px rgba(0, 0, 0, .1); 38 | $dropdown-border-width: 1px; 39 | $dropdown-border-color: transparent; 40 | $dropdown-item-padding-y: .5rem; 41 | $card-spacer-x: 1.5rem; 42 | $card-spacer-y: 1rem; 43 | $card-border-width: 1px; 44 | $card-border-color: transparent; 45 | $modal-content-border-radius: 1rem; 46 | $fa-font-path: "../webfonts"; 47 | -------------------------------------------------------------------------------- /resources/screenshots/home.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarceauKa/shaark/2be59cc231dca58a02ea65d314d95ebbff4f6c42/resources/screenshots/home.jpg -------------------------------------------------------------------------------- /resources/ssl/dev.shaark.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEFTCCAv2gAwIBAgIJAPAlvITbSEPEMA0GCSqGSIb3DQEBCwUAMHkxCzAJBgNV 3 | BAYTAkZSMRYwFAYDVQQIDA1JbGUtZGUtRnJhbmNlMQ4wDAYDVQQHDAVQYXJpczEP 4 | MA0GA1UECgwGNDA0bGFiMRwwGgYJKoZIhvcNAQkBFg13ZWJANDA0bGFiLmZyMRMw 5 | EQYDVQQDDApkZXYuc2hhYXJrMB4XDTE5MTExNDEwMzU0NloXDTIwMTExMzEwMzU0 6 | NloweTELMAkGA1UEBhMCRlIxFjAUBgNVBAgMDUlsZS1kZS1GcmFuY2UxDjAMBgNV 7 | BAcMBVBhcmlzMQ8wDQYDVQQKDAY0MDRsYWIxHDAaBgkqhkiG9w0BCQEWDXdlYkA0 8 | MDRsYWIuZnIxEzARBgNVBAMMCmRldi5zaGFhcmswggEiMA0GCSqGSIb3DQEBAQUA 9 | A4IBDwAwggEKAoIBAQDOeyhxP75JJ5KHKoacVr5JLV2QOizhImWnQtsT6oi1DRD+ 10 | KFyiugHQkMlPx92RE0HIp8juTyzkD5JZEGge4iVWwGHWpYgSafmyZFs9dw+D/ETG 11 | BJ3ac5nm/53tadCBOfzw3L7SmpHTNHjDR87BLLBOaEfmjZGQJFgqdxBkrcJETWxq 12 | 0d9sNWV4iIe/2YPmCpXTmgXl1vYovNI9BNgRcwc876NukVHY3nn+d6O/WN4ec0pQ 13 | M1eevJNBeT5Aj1ThJco/0CnTme5n3U7F7XqRrIVImlWyv9Jhu8KAw4V5+ps976fi 14 | yEP19e4JbUsA2ZizAFVwUYz7guszNxcYbdF9fFRNAgMBAAGjgZ8wgZwwHQYDVR0O 15 | BBYEFIDddQnfmFy6lHjCnnpg2Yt/UMRxMB8GA1UdIwQYMBaAFIDddQnfmFy6lHjC 16 | nnpg2Yt/UMRxMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgWgMBMGA1UdJQQMMAoGCCsG 17 | AQUFBwMBMC0GA1UdEQQmMCSCCmRldi5zaGFhcmuCFmRldi1zaGFhcmtBdEhpdmUu 18 | bG9jYWwwDQYJKoZIhvcNAQELBQADggEBAJzc9bA1wH6V0h2FStJGDbr6ZdKdar8e 19 | 7AWnMpY+g3turJHUdvKkuVfdWlwyhSv6etViBi9q0m6pKrpLKz75dbyXCTzzhr4u 20 | BEZ1QVqkumDabIO9K2dgryWHE8twxdEfJoWl5kh/u3dPpP6VUnia0a1cLAeZvyUy 21 | B0OaAc0uFL76eWgUK7H3AVLMsjHSitBmpXz20pDNSh9yelvDJ8Jj40p95zXj3cKh 22 | kFJW8WqTeQqXZ5SaxVxsExEZ0XGySHf3lgHcD5VQbrk1A9/VNAnYATYVQY0VBpDo 23 | M5pgUQisp9M95PU0hs7PPY1wQn8AEi7f5AuOBdQi1JPVqv8hl5dVlVE= 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /resources/views/album.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @push('meta') 4 | 5 | 6 | 7 | 8 | 9 | @endpush 10 | 11 | @section('content') 12 |
13 |
14 |
15 | 16 | @can('comments.see') 17 | 25 | @endcan 26 |
27 |
28 |
29 | @endsection 30 | -------------------------------------------------------------------------------- /resources/views/chest.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @push('meta') 4 | 5 | 6 | 7 | @endpush 8 | 9 | @section('content') 10 |
11 |
12 |
13 | 14 | @can('comments.see') 15 | 23 | @endcan 24 |
25 |
26 |
27 | @endsection 28 | -------------------------------------------------------------------------------- /resources/views/error.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.minimal') 2 | 3 | @section('content') 4 |
5 |
6 |
7 |
8 |
9 | @yield('code') - @yield('title') 10 | 11 | 12 | 13 |
14 | 15 |
16 |

@yield('message')

17 |
18 |
19 |
20 |
21 |
22 | @endsection 23 | -------------------------------------------------------------------------------- /resources/views/errors/401.blade.php: -------------------------------------------------------------------------------- 1 | @extends('error') 2 | 3 | @section('title', __('Unauthorized')) 4 | @section('code', '401') 5 | @section('message', __('Sorry, the page you are looking for could not be found.')) 6 | -------------------------------------------------------------------------------- /resources/views/errors/403.blade.php: -------------------------------------------------------------------------------- 1 | @extends('error') 2 | 3 | @section('title', __('Forbidden')) 4 | @section('code', '403') 5 | @section('message', __('Sorry, you are forbidden from accessing this page.')) 6 | -------------------------------------------------------------------------------- /resources/views/errors/404.blade.php: -------------------------------------------------------------------------------- 1 | @extends('error') 2 | 3 | @section('title', __('Page Not Found')) 4 | @section('code', '404') 5 | @section('message', __('Sorry, the page you are looking for could not be found.')) 6 | -------------------------------------------------------------------------------- /resources/views/errors/419.blade.php: -------------------------------------------------------------------------------- 1 | @extends('error') 2 | 3 | @section('title', __('Page Expired')) 4 | @section('code', '419') 5 | @section('message', __('Sorry, your session has expired. Please refresh and try again.')) 6 | -------------------------------------------------------------------------------- /resources/views/errors/429.blade.php: -------------------------------------------------------------------------------- 1 | @extends('error') 2 | 3 | @section('title', __('Too Many Requests')) 4 | @section('code', '429') 5 | @section('message', __('Sorry, you are making too many requests to our servers.')) 6 | -------------------------------------------------------------------------------- /resources/views/errors/500.blade.php: -------------------------------------------------------------------------------- 1 | @extends('error') 2 | 3 | @section('title', __('Server Error')) 4 | @section('code', '500') 5 | @section('message', __('Whoops, something went wrong on our servers.')) 6 | -------------------------------------------------------------------------------- /resources/views/errors/503.blade.php: -------------------------------------------------------------------------------- 1 | @extends('error') 2 | 3 | @section('title', __('Service Unavailable')) 4 | @section('code', '503') 5 | @section('message', __($exception->getMessage() ?: 'Service Unavailable')) 6 | -------------------------------------------------------------------------------- /resources/views/feed/atom.blade.php: -------------------------------------------------------------------------------- 1 | '.PHP_EOL ?> 2 | 3 | 4 | <![CDATA[{{ $title }}]]> 5 | 6 | {{ $language }} 7 | {{ $pub_date }} 8 | @foreach($items as $item) 9 | 10 | <![CDATA[{{ $item['title'] }}]]> 11 | 12 | {{ $item['id'] }} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {{ $item['created_at']->toRssString() }} 23 | 24 | @endforeach 25 | 26 | -------------------------------------------------------------------------------- /resources/views/feed/rss.blade.php: -------------------------------------------------------------------------------- 1 | '.PHP_EOL ?> 2 | 3 | 4 | <![CDATA[{{ $title }}]]> 5 | 6 | 7 | {{ $language }} 8 | {{ $pub_date }} 9 | @foreach($items as $item) 10 | 11 | <![CDATA[{{ $item['title'] }}]]> 12 | {{ $item['url'] }} 13 | 14 | 15 | {{ $item['id'] }} 16 | {{ $item['created_at']->toRssString() }} 17 | 18 | @endforeach 19 | 20 | 21 | -------------------------------------------------------------------------------- /resources/views/form-album.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |
6 |
7 | 8 |
9 |
10 |
11 | @endsection 12 | -------------------------------------------------------------------------------- /resources/views/form-chest.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |
6 |
7 | 8 |
9 |
10 |
11 | @endsection 12 | -------------------------------------------------------------------------------- /resources/views/form-link.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |
6 |
7 | 9 | 10 |
11 | 12 | @if(empty($query)) 13 |
14 | 15 |
16 | @endif 17 |
18 |
19 | @endsection 20 | -------------------------------------------------------------------------------- /resources/views/form-story.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @section('content') 4 |
5 |
6 |
7 | 8 |
9 |
10 |
11 | @endsection 12 | -------------------------------------------------------------------------------- /resources/views/layouts/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @include('layouts.partials.head') 5 | 6 | 7 |
8 | @include('layouts.partials.navbar') 9 |
10 | @yield('content') 11 |
12 | @include('layouts.partials.footer') 13 |
14 | @include('layouts.partials.scripts') 15 | 16 | 17 | -------------------------------------------------------------------------------- /resources/views/layouts/minimal.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @include('layouts.partials.head') 5 | 6 | 7 |
8 |
9 | @yield('content') 10 |
11 |
12 | @include('layouts.partials.scripts') 13 | 14 | 15 | -------------------------------------------------------------------------------- /resources/views/layouts/partials/footer.blade.php: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 | {{ __('Network is offline') }} 15 |
16 | 17 | @if(auth()->check()) 18 | 19 | @if($version) 20 | 21 | @endif 22 | @endif 23 | @include('partials.flash') 24 | -------------------------------------------------------------------------------- /resources/views/layouts/partials/head.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @if(isset($page_title)){{ $page_title }} - @endif{{ app('shaark')->getName() }} 5 | 6 | @if(auth()->check()) 7 | 8 | @endif 9 | @can('restricted') 10 | 11 | 12 | @endCan 13 | @stack('meta') 14 | 15 | 16 | 17 | 18 | 19 | @stack('css') 20 | @if(app('shaark')->getCustomBackgroundCss()) 21 | 30 | @endif 31 | @if(app('shaark')->getAdditionalCss()){!! app('shaark')->getAdditionalCss() !!}@endif 32 | -------------------------------------------------------------------------------- /resources/views/layouts/partials/scripts.blade.php: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | @if(app('shaark')->getAdditionalJs()){!! app('shaark')->getAdditionalJs() !!}@endif 9 | @stack('js') 10 | -------------------------------------------------------------------------------- /resources/views/link.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @push('meta') 4 | 5 | 6 | 7 | 8 | 9 | @endpush 10 | 11 | @section('content') 12 |
13 |
14 |
15 | 16 | @can('comments.see') 17 | 25 | @endcan 26 |
27 |
28 |
29 | @endsection 30 | -------------------------------------------------------------------------------- /resources/views/manage/archives.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.manage') 2 | 3 | @section('content') 4 |
5 |
6 | 7 |
8 |
9 | @endsection 10 | -------------------------------------------------------------------------------- /resources/views/manage/links.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.manage') 2 | 3 | @section('content') 4 | 6 | @endsection 7 | -------------------------------------------------------------------------------- /resources/views/manage/tags.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.manage') 2 | 3 | @section('content') 4 |
5 |
6 | 7 |
8 |
9 | @endsection 10 | -------------------------------------------------------------------------------- /resources/views/manage/users.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.manage') 2 | 3 | @section('content') 4 |
5 |
6 | 7 |
8 |
9 | @endsection 10 | -------------------------------------------------------------------------------- /resources/views/manage/walls.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.manage') 2 | 3 | @section('content') 4 |
5 |
6 | 7 |
8 |
9 | @endsection 10 | -------------------------------------------------------------------------------- /resources/views/offline.blade.php: -------------------------------------------------------------------------------- 1 | Offline {{ $page_title }} 2 | -------------------------------------------------------------------------------- /resources/views/partials/flash.blade.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/views/story.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.app') 2 | 3 | @push('meta') 4 | 5 | 6 | 7 | 8 | 9 | @endpush 10 | 11 | @section('content') 12 |
13 |
14 |
15 | 16 | @can('comments.see') 17 | 25 | @endcan 26 |
27 |
28 |
29 | @endsection 30 | -------------------------------------------------------------------------------- /resources/views/vendor/pagination/simple-bootstrap-4.blade.php: -------------------------------------------------------------------------------- 1 | @if ($paginator->hasPages()) 2 | 27 | @endif 28 | -------------------------------------------------------------------------------- /routes/channels.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | $uri = urldecode( 11 | parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) 12 | ); 13 | 14 | // This file allows us to emulate Apache's "mod_rewrite" functionality from the 15 | // built-in PHP web server. This provides a convenient way to test a Laravel 16 | // application without having installed a "real" web server software here. 17 | if ($uri !== '/' && file_exists(__DIR__.'/public'.$uri)) { 18 | return false; 19 | } 20 | 21 | require_once __DIR__.'/public/index.php'; 22 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !public/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/.gitignore: -------------------------------------------------------------------------------- 1 | config.php 2 | routes.php 3 | schedule-* 4 | compiled.php 5 | services.json 6 | events.scanned.php 7 | routes.scanned.php 8 | down 9 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/Browser/console/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/Browser/screenshots/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 19 | 20 | return $app; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/DuskTestCase.php: -------------------------------------------------------------------------------- 1 | shaark()->setLocale('en'); 20 | $this->shaark()->setSecureLogin(false); 21 | } 22 | 23 | /** 24 | * @beforeClass 25 | */ 26 | public static function prepare() 27 | { 28 | static::startChromeDriver(); 29 | } 30 | 31 | protected function driver() 32 | { 33 | $options = (new ChromeOptions)->addArguments([ 34 | '--disable-gpu', 35 | '--headless', 36 | '--window-size=1920,1080', 37 | ]); 38 | 39 | return RemoteWebDriver::create( 40 | 'http://localhost:9515', DesiredCapabilities::chrome()->setCapability( 41 | ChromeOptions::CAPABILITY, $options 42 | ) 43 | ); 44 | } 45 | 46 | protected function shaark(): Shaark 47 | { 48 | return app(Shaark::class); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class))->bootstrap(); 20 | foreach ($commands as $command) { 21 | $console->call($command); 22 | } -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | const mix = require('laravel-mix'); 2 | 3 | mix.js('resources/js/app.js', 'public/js') 4 | .extract([ 5 | 'axios', 6 | 'bootstrap', 7 | 'codemirror', 8 | 'highlight.js', 9 | 'jquery', 10 | 'lodash', 11 | 'mdurl', 12 | 'popper.js', 13 | 'sortablejs', 14 | 'to-mark', 15 | 'tui-code-snippet', 16 | 'tui-editor', 17 | 'vue-filepond', 18 | 'vue', 19 | 'v-img', 20 | 'vue-clickaway', 21 | 'vue-masonry-css', 22 | 'vue-multiselect', 23 | 'vue-toasted', 24 | 'vuedraggable', 25 | ]) 26 | .sass('resources/sass/app.scss', 'public/css') 27 | .version(); 28 | --------------------------------------------------------------------------------