1
0
Fork 0

Compare commits

..

27 Commits

Author SHA1 Message Date
hnrd 17a6c3931d Link legal notice
local jurisdiction requires a prominent link to a legal notice at the frontpage.
2023-10-08 10:37:14 +02:00
hnrd e860abb64b allow 30 char usernames
raise maximum username length, because why not?
2023-10-08 10:36:19 +02:00
hnrd 13995f0612 disable beagle service
beagle is a remote API service provided by dansup and used for centralised lookups.
Using the beagle service without users explicit consent violates GDPR.
As it's not configurable at the moment this patch disables remote communication with beagle.
2023-10-08 10:32:53 +02:00
hnrd 812e130d0e point to modified sourcecode
as per AGPL license of original source, modifications must be disclosed.
2023-10-08 10:32:46 +02:00
hnrd 2e602bea61 hardcode discovery settings
force enable discovery (as dynamic settings are not saved properly)
2023-10-08 10:27:34 +02:00
hnrd 983309a3f9 remove IP logging
Replace unneeded logging of IPs and User-Agent strings with meaningless static data.
2023-10-08 10:25:54 +02:00
Daniel Supernault 6f6ebe7d43 Update changelog 2023-10-08 10:25:54 +02:00
Daniel Supernault 0b43367a2a Update compiled assets 2023-10-08 10:25:54 +02:00
Daniel Supernault 8d24020dc5 Update PostContent, add text cw warning 2023-10-08 10:25:54 +02:00
Daniel Supernault 4ff229bce0 Update changelog 2023-10-08 10:25:54 +02:00
Daniel Supernault 1c92daf925 Update compiled assets 2023-10-08 10:25:54 +02:00
Daniel Supernault 18004040aa Update timeline settings, add photo reblogs only option 2023-10-08 10:25:54 +02:00
Daniel Supernault 92e030de86 Update SettingsController, add photo_reblogs_only setting 2023-10-08 10:25:54 +02:00
Daniel Supernault 97d5aea697 Update changelog 2023-10-08 10:25:54 +02:00
Daniel Supernault fd766b00cb Update compiled assets 2023-10-08 10:25:54 +02:00
Daniel Supernault 536912d237 Update Timeline component, improve reblog support 2023-10-08 10:25:54 +02:00
Daniel Supernault 925f5e8df2 Update SharePipeline 2023-10-08 10:25:54 +02:00
Daniel Supernault ec335288df Update SharePipeline 2023-10-08 10:25:54 +02:00
Daniel Supernault e15cb221db Update ApiV1Controller, hydrate reblog state in home timeline 2023-10-08 10:25:54 +02:00
Daniel Supernault bf2b1a003d Update changelog 2023-10-08 10:25:54 +02:00
Daniel Supernault 45bdd410fb Update compiled assets 2023-10-08 10:25:54 +02:00
Daniel Supernault 40b875e21e Update Timeline component 2023-10-08 10:25:54 +02:00
Daniel Supernault e1255c07fb Update ApiV1Dot1Controller 2023-10-08 10:25:54 +02:00
Daniel Supernault 0bc4b457f5 Update timeline settings 2023-10-08 10:25:54 +02:00
Daniel Supernault 0632034c40 Update SettingsController 2023-10-08 10:25:54 +02:00
Daniel Supernault 4ad484d691 Update StatusStatelessTransformer, allow unlisted reblogs 2023-10-08 10:25:54 +02:00
Daniel Supernault 0e72d557fe Update admin users view, fix website value. Closes #4557 2023-10-08 10:25:54 +02:00
577 changed files with 22917 additions and 49565 deletions

View File

@ -21,12 +21,7 @@ jobs:
steps:
- checkout
- run:
name: "Create Environment file and generate app key"
command: |
mv .env.testing .env
- run: sudo apt install zlib1g-dev libsqlite3-dev
- run: sudo apt update && sudo apt install zlib1g-dev libsqlite3-dev
# Download and cache dependencies
@ -41,17 +36,18 @@ jobs:
- run: composer install -n --prefer-dist
- save_cache:
key: v2-dependencies-{{ checksum "composer.json" }}
key: composer-v2-{{ checksum "composer.lock" }}
paths:
- vendor
- run: cp .env.testing .env
- run: php artisan config:cache
- run: php artisan route:clear
- run: php artisan storage:link
- run: php artisan key:generate
# run tests with phpunit or codecept
- run: php artisan test
- run: ./vendor/bin/phpunit
- store_test_results:
path: tests/_output
- store_artifacts:

View File

@ -4,4 +4,4 @@
## Usage: redis-cli [flags] [args]
## Example: "redis-cli KEYS *" or "ddev redis-cli INFO" or "ddev redis-cli --version"
exec redis-cli -p 6379 -h redis "$@"
redis-cli -p 6379 -h redis $@

View File

@ -1,30 +1,8 @@
.DS_Store
/.bash_history
/.bash_profile
/.bashrc
/.composer
/.env
/.env.dottie-backup
/.git
/.git-credentials
/.gitconfig
/.gitignore
/.idea
/.vagrant
/bootstrap/cache
/docker-compose-state/
/Homestead.json
/Homestead.yaml
/node_modules
/npm-debug.log
/public/hot
/public/storage
/public/vendor/horizon
/storage/*.key
/storage/docker
/vendor
/yarn-error.log
# Exceptions - these *MUST* be last
!/bootstrap/cache/.gitignore
!/public/vendor/horizon/.gitignore
data
Dockerfile
contrib/docker/Dockerfile.*
docker-compose*.yml
.dockerignore
.git
.gitignore
.env

View File

@ -1,27 +1,9 @@
root = true
[*]
indent_style = space
indent_size = 4
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.{yml,yaml}]
indent_style = space
indent_size = 2
[*.{sh,envsh,env,env*}]
indent_style = space
indent_size = 4
# ShellCheck config
shell_variant = bash # like -ln=bash
binary_next_line = true # like -bn
switch_case_indent = true # like -ci
space_redirects = false # like -sr
keep_padding = false # like -kp
function_next_line = true # like -fn
never_split = true # like -ns
simplify = true

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,6 @@ OPEN_REGISTRATION="false"
ENFORCE_EMAIL_VERIFICATION="false"
PF_MAX_USERS="1000"
OAUTH_ENABLED="true"
ENABLE_CONFIG_CACHE=true
# Media Configuration
PF_OPTIMIZE_IMAGES="true"

View File

@ -1,5 +1,3 @@
# shellcheck disable=SC2034,SC2148
APP_NAME="Pixelfed Test"
APP_ENV=local
APP_KEY=base64:lwX95GbNWX3XsucdMe0XwtOKECta3h/B+p9NbH2jd0E=

7
.gitattributes vendored
View File

@ -3,10 +3,3 @@
*.scss linguist-vendored
*.js linguist-vendored
CHANGELOG.md export-ignore
# Collapse diffs for generated files:
public/**/*.js text -diff
public/**/*.json text -diff
public/**/*.css text -diff
public/img/* binary -diff
public/fonts/* binary -diff

125
.github/workflows/build-docker.yml vendored Normal file
View File

@ -0,0 +1,125 @@
---
name: Build Docker image
on:
workflow_dispatch:
push:
branches:
- dev
tags:
- '*'
pull_request:
paths:
- .github/workflows/build-docker.yml
- contrib/docker/Dockerfile.apache
- contrib/docker/Dockerfile.fpm
permissions:
contents: read
jobs:
build-docker-apache:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Docker Lint
uses: hadolint/hadolint-action@v3.0.0
with:
dockerfile: contrib/docker/Dockerfile.apache
failure-threshold: error
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
secrets: inherit
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
if: github.event_name != 'pull_request'
- name: Fetch tags
uses: docker/metadata-action@v4
secrets: inherit
id: meta
with:
images: ${{ secrets.DOCKER_HUB_ORGANISATION }}/pixelfed
flavor: |
latest=auto
suffix=-apache
tags: |
type=edge,branch=dev
type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}}
type=ref,event=pr
- name: Build and push Docker image
uses: docker/build-push-action@v3
with:
context: .
file: contrib/docker/Dockerfile.apache
platforms: linux/amd64,linux/arm64
builder: ${{ steps.buildx.outputs.name }}
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-docker-fpm:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Docker Lint
uses: hadolint/hadolint-action@v3.0.0
with:
dockerfile: contrib/docker/Dockerfile.fpm
failure-threshold: error
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
secrets: inherit
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
if: github.event_name != 'pull_request'
- name: Fetch tags
uses: docker/metadata-action@v4
secrets: inherit
id: meta
with:
images: ${{ secrets.DOCKER_HUB_ORGANISATION }}/pixelfed
flavor: |
suffix=-fpm
tags: |
type=edge,branch=dev
type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}}
type=ref,event=pr
- name: Build and push Docker image
uses: docker/build-push-action@v3
with:
context: .
file: contrib/docker/Dockerfile.fpm
platforms: linux/amd64,linux/arm64
builder: ${{ steps.buildx.outputs.name }}
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@ -1,230 +0,0 @@
---
name: Docker
on:
# See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch
workflow_dispatch:
# See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#push
push:
branches:
- dev
- staging
tags:
- "*"
# See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
pull_request:
types:
- opened
- reopened
- synchronize
jobs:
lint:
name: hadolint
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Docker Lint
uses: hadolint/hadolint-action@v3.1.0
with:
dockerfile: Dockerfile
failure-threshold: error
shellcheck:
name: ShellCheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@master
env:
SHELLCHECK_OPTS: --shell=bash --external-sources
with:
version: v0.9.0
additional_files: "*.envsh .env .env.docker .env.example .env.testing"
bats:
name: Bats Testing
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run bats
run: docker run -v "$PWD:/var/www" bats/bats:latest /var/www/tests/bats
build:
name: Build, Test, and Push
runs-on: ubuntu-latest
strategy:
fail-fast: false
# See: https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs
matrix:
php_version:
- 8.2
- 8.3
target_runtime:
- apache
- fpm
- nginx
php_base:
- apache
- fpm
# See: https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs#excluding-matrix-configurations
# See: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategymatrixexclude
exclude:
# targeting [apache] runtime with [fpm] base type doesn't make sense
- target_runtime: apache
php_base: fpm
# targeting [fpm] runtime with [apache] base type doesn't make sense
- target_runtime: fpm
php_base: apache
# targeting [nginx] runtime with [apache] base type doesn't make sense
- target_runtime: nginx
php_base: apache
# See: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-using-concurrency-and-the-default-behavior
concurrency:
group: docker-build-${{ github.ref }}-${{ matrix.php_base }}-${{ matrix.php_version }}-${{ matrix.target_runtime }}
cancel-in-progress: true
permissions:
contents: read
packages: write
env:
# Set the repo variable [DOCKER_HUB_USERNAME] to override the default
# at https://github.com/<user>/<project>/settings/variables/actions
DOCKER_HUB_USERNAME: ${{ vars.DOCKER_HUB_USERNAME || 'pixelfed' }}
# Set the repo variable [DOCKER_HUB_ORGANISATION] to override the default
# at https://github.com/<user>/<project>/settings/variables/actions
DOCKER_HUB_ORGANISATION: ${{ vars.DOCKER_HUB_ORGANISATION || 'pixelfed' }}
# Set the repo variable [DOCKER_HUB_REPO] to override the default
# at https://github.com/<user>/<project>/settings/variables/actions
DOCKER_HUB_REPO: ${{ vars.DOCKER_HUB_REPO || 'pixelfed' }}
# For Docker Hub pushing to work, you need the secret [DOCKER_HUB_TOKEN]
# set to your Personal Access Token at https://github.com/<user>/<project>/settings/secrets/actions
#
# ! NOTE: no [login] or [push] will happen to Docker Hub until this secret is set!
HAS_DOCKER_HUB_CONFIGURED: ${{ secrets.DOCKER_HUB_TOKEN != '' }}
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
id: buildx
with:
version: v0.12.0 # *or* newer, needed for annotations to work
# See: https://github.com/docker/login-action?tab=readme-ov-file#github-container-registry
- name: Log in to the GitHub Container registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# See: https://github.com/docker/login-action?tab=readme-ov-file#docker-hub
- name: Login to Docker Hub registry (conditionally)
if: ${{ env.HAS_DOCKER_HUB_CONFIGURED == true }}
uses: docker/login-action@v3
with:
username: ${{ env.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Docker meta
uses: docker/metadata-action@v5
id: meta
with:
images: |
name=ghcr.io/${{ github.repository }},enable=true
name=${{ env.DOCKER_HUB_ORGANISATION }}/${{ env.DOCKER_HUB_REPO }},enable=${{ env.HAS_DOCKER_HUB_CONFIGURED }}
flavor: |
latest=auto
suffix=-${{ matrix.target_runtime }}-${{ matrix.php_version }}
tags: |
type=raw,value=dev,enable=${{ github.ref == format('refs/heads/{0}', 'dev') }}
type=raw,value=staging,enable=${{ github.ref == format('refs/heads/{0}', 'staging') }}
type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}}
type=ref,event=branch,prefix=branch-
type=ref,event=pr,prefix=pr-
type=ref,event=tag
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
- name: Docker meta (Cache)
uses: docker/metadata-action@v5
id: cache
with:
images: |
name=ghcr.io/${{ github.repository }}-cache,enable=true
name=${{ env.DOCKER_HUB_ORGANISATION }}/${{ env.DOCKER_HUB_REPO }}-cache,enable=${{ env.HAS_DOCKER_HUB_CONFIGURED }}
flavor: |
latest=auto
suffix=-${{ matrix.target_runtime }}-${{ matrix.php_version }}
tags: |
type=raw,value=dev,enable=${{ github.ref == format('refs/heads/{0}', 'dev') }}
type=raw,value=staging,enable=${{ github.ref == format('refs/heads/{0}', 'staging') }}
type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}}
type=ref,event=branch,prefix=branch-
type=ref,event=pr,prefix=pr-
type=ref,event=tag
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile
target: ${{ matrix.target_runtime }}-runtime
platforms: linux/amd64,linux/arm64
builder: ${{ steps.buildx.outputs.name }}
tags: ${{ steps.meta.outputs.tags }}
annotations: ${{ steps.meta.outputs.annotations }}
push: true
sbom: true
provenance: true
build-args: |
PHP_VERSION=${{ matrix.php_version }}
PHP_BASE_TYPE=${{ matrix.php_base }}
cache-from: |
type=gha,scope=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }}
cache-to: |
type=gha,mode=max,scope=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }}
${{ steps.cache.outputs.tags }}
# goss validate the image
#
# See: https://github.com/goss-org/goss
- uses: e1himself/goss-installation-action@v1
with:
version: "v0.4.4"
- name: Execute Goss tests
run: |
dgoss run \
-v "./.env.testing:/var/www/.env" \
-e "EXPECTED_PHP_VERSION=${{ matrix.php_version }}" \
-e "PHP_BASE_TYPE=${{ matrix.php_base }}" \
${{ steps.meta.outputs.tags }}

43
.gitignore vendored
View File

@ -1,31 +1,22 @@
.DS_Store
/.bash_history
/.bash_profile
/.bashrc
/.composer
/.env
/.env.dottie-backup
#/.git
/.git-credentials
/.gitconfig
#/.gitignore
/.idea
/.vagrant
/bootstrap/cache
/docker-compose-state/
/Homestead.json
/Homestead.yaml
/node_modules
/npm-debug.log
/public/hot
/public/storage
/public/vendor/horizon
/storage/*.key
/storage/docker
/vendor
/yarn-error.log
/public/build
# Exceptions - these *MUST* be last
!/bootstrap/cache/.gitignore
!/public/vendor/horizon/.gitignore
/.idea
/.vscode
/.vagrant
/docker-volumes
Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
.env
.DS_Store
.bash_profile
.bash_history
.bashrc
.gitconfig
.git-credentials
/.composer/
/nginx.conf

View File

@ -1,6 +0,0 @@
ignored:
- DL3002 # warning: Last USER should not be root
- DL3008 # warning: Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`
- DL3029 # warning: Do not use --platform flag with FROM
- SC2046 # warning: Quote this to prevent word splitting.
- SC2086 # info: Double quote to prevent globbing and word splitting.

View File

@ -1,4 +0,0 @@
{
"MD013": false,
"MD014": false
}

View File

@ -1,12 +0,0 @@
# See: https://github.com/koalaman/shellcheck/blob/master/shellcheck.1.md#rc-files
source-path=SCRIPTDIR
# Allow opening any 'source'd file, even if not specified as input
external-sources=true
# Turn on warnings for unquoted variables with safe values
enable=quote-safe-variables
# Turn on warnings for unassigned uppercase variables
enable=check-unassigned-uppercase

View File

@ -1,14 +0,0 @@
{
"recommendations": [
"foxundermoon.shell-format",
"timonwong.shellcheck",
"jetmartin.bats",
"aaron-bond.better-comments",
"streetsidesoftware.code-spell-checker",
"editorconfig.editorconfig",
"github.vscode-github-actions",
"bmewburn.vscode-intelephense-client",
"redhat.vscode-yaml",
"ms-azuretools.vscode-docker"
]
}

21
.vscode/settings.json vendored
View File

@ -1,21 +0,0 @@
{
"shellformat.useEditorConfig": true,
"[shellscript]": {
"files.eol": "\n",
"editor.defaultFormatter": "foxundermoon.shell-format"
},
"[yaml]": {
"editor.defaultFormatter": "redhat.vscode-yaml"
},
"[dockercompose]": {
"editor.defaultFormatter": "redhat.vscode-yaml",
"editor.autoIndent": "advanced",
},
"yaml.schemas": {
"https://json.schemastore.org/composer": "https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json"
},
"files.associations": {
".env": "shellscript",
".env.*": "shellscript"
}
}

View File

@ -1,268 +1,12 @@
# Release Notes
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.12.1...dev)
### Updates
- Update DirectMessageController, add 72 hour delay for new accounts before they can send a DM ([61d105fd](https://github.com/pixelfed/pixelfed/commit/61d105fd))
- Update AdminCuratedRegisterController, increase message length from 1000 to 3000 ([9a5e3471](https://github.com/pixelfed/pixelfed/commit/9a5e3471))
- Update ApiV1Controller, add pe (pixelfed entity) support to /api/v1/statuses/{id}/context endpoint ([d645d6ca](https://github.com/pixelfed/pixelfed/commit/d645d6ca))
- Update Admin Curated Onboarding, add select-all/mass action operations ([b22cac94](https://github.com/pixelfed/pixelfed/commit/b22cac94))
- Update AdminCuratedRegisterController, fix existing account approval ([cbb96cfd](https://github.com/pixelfed/pixelfed/commit/cbb96cfd))
- Update ActivityPubFetchService, fix Friendica bug ([e4edc6f1](https://github.com/pixelfed/pixelfed/commit/e4edc6f1))
- Update ProfileController, fix atom feed cache ttl. Fixes #5093 ([921e2965](https://github.com/pixelfed/pixelfed/commit/921e2965))
- Update CollectionsController, add new self route ([bc2495c6](https://github.com/pixelfed/pixelfed/commit/bc2495c6))
- Update FederationController, add webfinger support for actor uri. Fixes #5068 ([24194f7d](https://github.com/pixelfed/pixelfed/commit/24194f7d))
- ([](https://github.com/pixelfed/pixelfed/commit/))
- ([](https://github.com/pixelfed/pixelfed/commit/))
- ([](https://github.com/pixelfed/pixelfed/commit/))
## [v0.12.1 (2024-05-07)](https://github.com/pixelfed/pixelfed/compare/v0.12.0...v0.12.1)
### Updates
- Update ApiV1Dot1Controller, fix in app registration bug that prevents proper auth flow due to missing oauth scopes ([cbf996c9](https://github.com/pixelfed/pixelfed/commit/cbf996c9))
- Update ConfigCacheService, fix database race condition and fallback to file config and enable by default ([60a62b59](https://github.com/pixelfed/pixelfed/commit/60a62b59))
## [v0.12.0 (2024-04-29)](https://github.com/pixelfed/pixelfed/compare/v0.11.13...v0.12.0)
### Updates
- Update SoftwareUpdateService, add command to refresh latest versions ([632f2cb6](https://github.com/pixelfed/pixelfed/commit/632f2cb6))
- Update Post.vue, fix cache bug ([3a27e637](https://github.com/pixelfed/pixelfed/commit/3a27e637))
- Update StatusHashtagService, use more efficient cached count ([592c8412](https://github.com/pixelfed/pixelfed/commit/592c8412))
- Update DiscoverController, handle discover hashtag redirects ([18382e8a](https://github.com/pixelfed/pixelfed/commit/18382e8a))
- Update ApiV1Controller, use admin filter service ([94503a1c](https://github.com/pixelfed/pixelfed/commit/94503a1c))
- Update SearchApiV2Service, use more efficient query ([cee618e8](https://github.com/pixelfed/pixelfed/commit/cee618e8))
- Update Curated Onboarding view, fix concierge form ([15ad69f7](https://github.com/pixelfed/pixelfed/commit/15ad69f7))
- Update AP Profile Transformer, add `suspended` attribute ([25f3fa06](https://github.com/pixelfed/pixelfed/commit/25f3fa06))
- Update AP Profile Transformer, fix movedTo attribute ([63100fe9](https://github.com/pixelfed/pixelfed/commit/63100fe9))
- Update AP Profile Transformer, fix suspended attributes ([2e5e68e4](https://github.com/pixelfed/pixelfed/commit/2e5e68e4))
- Update PrivacySettings controller, add cache invalidation ([e742d595](https://github.com/pixelfed/pixelfed/commit/e742d595))
- Update ProfileController, preserve deleted actor objects for federated account deletion and use more efficient account cache lookup ([853a729f](https://github.com/pixelfed/pixelfed/commit/853a729f))
- Update SiteController, add curatedOnboarding method that gracefully falls back to open registration when applicable ([95199843](https://github.com/pixelfed/pixelfed/commit/95199843))
- Update AP transformers, add DeleteActor activity ([bcce1df6](https://github.com/pixelfed/pixelfed/commit/bcce1df6))
- Update commands, add user account delete cli command to federate account deletion ([4aa0e25f](https://github.com/pixelfed/pixelfed/commit/4aa0e25f))
- Update web-api popular accounts route to its own method to remove the breaking oauth scope bug ([a4bc5ce3](https://github.com/pixelfed/pixelfed/commit/a4bc5ce3))
- Update config cache ([5e4d4eff](https://github.com/pixelfed/pixelfed/commit/5e4d4eff))
- Update Config, use config_cache ([7785a2da](https://github.com/pixelfed/pixelfed/commit/7785a2da))
- Update ApiV1Dot1Controller, use config_cache for in-app registration ([b0cb4456](https://github.com/pixelfed/pixelfed/commit/b0cb4456))
- Update captcha, use config_cache helper ([8a89e3c9](https://github.com/pixelfed/pixelfed/commit/8a89e3c9))
- Update custom emoji, add config_cache support ([481314cd](https://github.com/pixelfed/pixelfed/commit/481314cd))
- Update ProfileController, fix permalink redirect bug ([75081e60](https://github.com/pixelfed/pixelfed/commit/75081e60))
- Update admin css, use font-display:swap for nucleo icons ([8a0c456e](https://github.com/pixelfed/pixelfed/commit/8a0c456e))
- Update PixelfedDirectoryController, fix boolean cast bug ([f08aab22](https://github.com/pixelfed/pixelfed/commit/f08aab22))
- Update PixelfedDirectoryController, use cached stats ([f2f2a809](https://github.com/pixelfed/pixelfed/commit/f2f2a809))
- Update AdminDirectoryController, fix type casting ([ad506e90](https://github.com/pixelfed/pixelfed/commit/ad506e90))
- Update image pipeline, use config_cache ([a72188a7](https://github.com/pixelfed/pixelfed/commit/a72188a7))
- Update cloud storage, use config_cache ([665581d8](https://github.com/pixelfed/pixelfed/commit/665581d8))
- Update pixelfed.max_album_length, use config_cache ([fecbe189](https://github.com/pixelfed/pixelfed/commit/fecbe189))
- Update media_types, use config_cache ([d670de17](https://github.com/pixelfed/pixelfed/commit/d670de17))
- Update landing settings, use config_cache ([40478f25](https://github.com/pixelfed/pixelfed/commit/40478f25))
- Update activitypub setting, use config_cache ([5071aaf4](https://github.com/pixelfed/pixelfed/commit/5071aaf4))
- Update oauth setting, use config_cache ([ce228f7f](https://github.com/pixelfed/pixelfed/commit/ce228f7f))
- Update stories config, use config_cache ([d1adb109](https://github.com/pixelfed/pixelfed/commit/d1adb109))
- Update ig import, use config_cache ([da0e0ffa](https://github.com/pixelfed/pixelfed/commit/da0e0ffa))
- Update autospam config, use config_cache ([a76cb5f4](https://github.com/pixelfed/pixelfed/commit/a76cb5f4))
- Update app.name config, use config_cache ([911446c0](https://github.com/pixelfed/pixelfed/commit/911446c0))
- Update UserObserver, fix type casting ([949e9979](https://github.com/pixelfed/pixelfed/commit/949e9979))
- Update user_filters, use config_cache ([6ce513f8](https://github.com/pixelfed/pixelfed/commit/6ce513f8))
- Update filesystems config, add to config_cache ([087b2791](https://github.com/pixelfed/pixelfed/commit/087b2791))
- Update web-admin routes, add setting api routes ([828a456f](https://github.com/pixelfed/pixelfed/commit/828a456f))
- Update hashtag component ([cee979ed](https://github.com/pixelfed/pixelfed/commit/cee979ed))
- Update AdminReadMore component, add .prevent to click action ([704e7b12](https://github.com/pixelfed/pixelfed/commit/704e7b12))
- Update admin dashboard, add admin settings partials ([eb487123](https://github.com/pixelfed/pixelfed/commit/eb487123))
- Update admin settings, refactor to vue component ([674e560f](https://github.com/pixelfed/pixelfed/commit/674e560f))
- Update ConfigCacheService, encrypt keys at rest ([3628b462](https://github.com/pixelfed/pixelfed/commit/3628b462))
- Update RemoteFollowImportRecent, use MediaPathService ([5162c070](https://github.com/pixelfed/pixelfed/commit/5162c070))
- Update AdminSettingsController, add user filter max limit settings ([ac1f0748](https://github.com/pixelfed/pixelfed/commit/ac1f0748))
- Update AdminSettingsController, add AdminSettingsService ([dcc5f416](https://github.com/pixelfed/pixelfed/commit/dcc5f416))
- Update AdminSettings component, fix user settings ([aba1e13d](https://github.com/pixelfed/pixelfed/commit/aba1e13d))
- Update AdminInstances component ([ec2fdd61](https://github.com/pixelfed/pixelfed/commit/ec2fdd61))
- Update AdminSettings, add max_account_size support ([2dcbc1d5](https://github.com/pixelfed/pixelfed/commit/2dcbc1d5))
- Update AdminSettings, use better validation for user integer settings ([d946afcc](https://github.com/pixelfed/pixelfed/commit/d946afcc))
- Update spa sass, fix timestamp dark mode bug ([4147f7c5](https://github.com/pixelfed/pixelfed/commit/4147f7c5))
- Update relationships view, fix unfollow hashtag bug. Fixes #5008 ([8c693640](https://github.com/pixelfed/pixelfed/commit/8c693640))
- Update PrivacySettings controller, refresh RelationshipService when unmute/unblocking ([b7322b68](https://github.com/pixelfed/pixelfed/commit/b7322b68))
- Update ApiV1Controller, improve refresh relations logic when (un)muting or (un)blocking ([b8e96a5f](https://github.com/pixelfed/pixelfed/commit/b8e96a5f))
- Update context menu, add mute/block/unfollow actions and update relationship store accordingly ([81d1e0fd](https://github.com/pixelfed/pixelfed/commit/81d1e0fd))
- Update docker env, fix config_cache. Fixes #5033 ([858fcbf6](https://github.com/pixelfed/pixelfed/commit/858fcbf6))
- Update UnfollowPipeline, fix follower count cache bug ([6bdf73de](https://github.com/pixelfed/pixelfed/commit/6bdf73de))
- Update VideoPresenter component, add webkit-playsinline attribute to video element to prevent the full screen video player ([ad032916](https://github.com/pixelfed/pixelfed/commit/ad032916))
- Update VideoPlayer component, add playsinline attribute to video element ([8af23607](https://github.com/pixelfed/pixelfed/commit/8af23607))
- Update StatusController, refactor status embeds ([9a7acc12](https://github.com/pixelfed/pixelfed/commit/9a7acc12))
- Update ProfileController, refactor profile embeds ([8b8b1ffc](https://github.com/pixelfed/pixelfed/commit/8b8b1ffc))
- Update profile embed view, fix height bug ([65166570](https://github.com/pixelfed/pixelfed/commit/65166570))
- Update CustomEmojiService, only return local emoji ([7f8bba44](https://github.com/pixelfed/pixelfed/commit/7f8bba44))
- Update Like model, increase max likes per day from 500 to 1500 ([4223119f](https://github.com/pixelfed/pixelfed/commit/4223119f))
## [v0.11.13 (2024-03-05)](https://github.com/pixelfed/pixelfed/compare/v0.11.12...v0.11.13)
### Features
- Account Migrations ([#4968](https://github.com/pixelfed/pixelfed/pull/4968)) ([4a6be6212](https://github.com/pixelfed/pixelfed/pull/4968/commits/4a6be6212))
- Curated Onboarding ([#4946](https://github.com/pixelfed/pixelfed/pull/4946)) ([8dac2caf](https://github.com/pixelfed/pixelfed/commit/8dac2caf))
- Add Curated Onboarding Templates ([071163b4](https://github.com/pixelfed/pixelfed/commit/071163b4))
- Add Remote Reports to Admin Dashboard Reports page ([ef0ff78e](https://github.com/pixelfed/pixelfed/commit/ef0ff78e))
- Improved Docker Support ([#4844](https://github.com/pixelfed/pixelfed/pull/4844)) ([d92cf7f](https://github.com/pixelfed/pixelfed/commit/d92cf7f))
### Updates
- Update Inbox, cast live filters to lowercase ([d835e0ad](https://github.com/pixelfed/pixelfed/commit/d835e0ad))
- Update federation config, increase default timeline days falloff to 90 days from 2 days. Fixes #4905 ([011834f4](https://github.com/pixelfed/pixelfed/commit/011834f4))
- Update cache config, use predis as default redis driver client ([ea6b1623](https://github.com/pixelfed/pixelfed/commit/ea6b1623))
- Update .gitattributes to collapse diffs on generated files ([ThisIsMissEm](https://github.com/pixelfed/pixelfed/commit/9978b2b9))
- Update api v1/v2 instance endpoints, bump mastoapi version from 2.7.2 to 3.5.3 ([545f7d5e](https://github.com/pixelfed/pixelfed/commit/545f7d5e))
- Update ApiV1Controller, implement better limit logic to gracefully handle requests with limits that exceed the max ([1f74a95d](https://github.com/pixelfed/pixelfed/commit/1f74a95d))
- Update AdminCuratedRegisterController, show oldest applications first ([c4dde641](https://github.com/pixelfed/pixelfed/commit/c4dde641))
- Update Directory logic, add curated onboarding support ([59c70239](https://github.com/pixelfed/pixelfed/commit/59c70239))
- Update Inbox and StatusObserver, fix silently rejected direct messages due to saveQuietly which failed to generate a snowflake id ([089ba3c4](https://github.com/pixelfed/pixelfed/commit/089ba3c4))
- Update Curated Onboarding dashboard, improve application filtering and make it easier to distinguish response state ([2b5d7235](https://github.com/pixelfed/pixelfed/commit/2b5d7235))
- Update AdminReports, add story reports and fix cs ([767522a8](https://github.com/pixelfed/pixelfed/commit/767522a8))
- Update AdminReportController, add story report support ([a16309ac](https://github.com/pixelfed/pixelfed/commit/a16309ac))
- Update kb, add email confirmation issues page ([2f48df8c](https://github.com/pixelfed/pixelfed/commit/2f48df8c))
- Update AdminCuratedRegisterController, filter confirmation activities from activitylog ([ab9ecb6e](https://github.com/pixelfed/pixelfed/commit/ab9ecb6e))
- Update Inbox, fix flag validation condition, allow profile reports ([402a4607](https://github.com/pixelfed/pixelfed/commit/402a4607))
- Update AccountTransformer, fix follower/following count visibility bug ([542d1106](https://github.com/pixelfed/pixelfed/commit/542d1106))
- Update ProfileMigration model, add target relation ([3f053997](https://github.com/pixelfed/pixelfed/commit/3f053997))
- Update ApiV1Controller, update Notifications endpoint to filter notifications with missing activities ([a933615b](https://github.com/pixelfed/pixelfed/commit/a933615b))
- Update ApiV1Controller, fix public timeline scope, properly support both local + remote parameters ([d6eac655](https://github.com/pixelfed/pixelfed/commit/d6eac655))
- Update ApiV1Controller, handle public feed parameter bug to gracefully fallback to min_id=1 when max_id=0 ([e3826c58](https://github.com/pixelfed/pixelfed/commit/e3826c58))
- Update ApiV1Controller, fix hashtag feed to include private posts from accounts you follow or your own, and your own unlisted posts ([3b5500b3](https://github.com/pixelfed/pixelfed/commit/3b5500b3))
- Update checkpoint view, improve input autocomplete. Fixes ([#4959](https://github.com/pixelfed/pixelfed/pull/4959)) ([d18824e7](https://github.com/pixelfed/pixelfed/commit/d18824e7))
- Update navbar.vue, removes the 50px limit ([#4969](https://github.com/pixelfed/pixelfed/pull/4969)) ([7fd5599](https://github.com/pixelfed/pixelfed/commit/7fd5599))
- Update ComposeModal.vue, add an informative UI error message when trying to create a mixed media album ([#4886](https://github.com/pixelfed/pixelfed/pull/4886)) ([fd4f41a](https://github.com/pixelfed/pixelfed/commit/fd4f41a))
## [v0.11.12 (2024-02-16)](https://github.com/pixelfed/pixelfed/compare/v0.11.11...v0.11.12)
### Features
- Autospam Live Filters - block remote activities based on comma separated keywords ([40b45b2a](https://github.com/pixelfed/pixelfed/commit/40b45b2a))
- Added Software Update banner to admin home feeds ([b0fb1988](https://github.com/pixelfed/pixelfed/commit/b0fb1988))
### Updates
- Update ApiV1Controller, fix network timeline ([0faf59e3](https://github.com/pixelfed/pixelfed/commit/0faf59e3))
- Update public/network timelines, fix non-redis response and fix reblogs in home feed ([8b4ac5cc](https://github.com/pixelfed/pixelfed/commit/8b4ac5cc))
- Update Federation, use proper Content-Type headers for following/follower collections ([fb0bb9a3](https://github.com/pixelfed/pixelfed/commit/fb0bb9a3))
- Update ActivityPubFetchService, enforce stricter Content-Type validation ([1232cfc8](https://github.com/pixelfed/pixelfed/commit/1232cfc8))
- Update status view, fix unlisted/private scope bug ([0f3ca194](https://github.com/pixelfed/pixelfed/commit/0f3ca194))
## [v0.11.11 (2024-02-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.10...v0.11.11)
### Fixes
- Fix api endpoints ([fd7f5dbb](https://github.com/pixelfed/pixelfed/commit/fd7f5dbb))
## [v0.11.10 (2024-02-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.9...v0.11.10)
### Added
- Resilient Media Storage ([#4665](https://github.com/pixelfed/pixelfed/pull/4665)) ([fb1deb6](https://github.com/pixelfed/pixelfed/commit/fb1deb6))
- Video WebP2P ([#4713](https://github.com/pixelfed/pixelfed/pull/4713)) ([0405ef12](https://github.com/pixelfed/pixelfed/commit/0405ef12))
- Added user:2fa command to easily disable 2FA for given account ([c6408fd7](https://github.com/pixelfed/pixelfed/commit/c6408fd7))
- Added `avatar:storage-deep-clean` command to dispatch remote avatar storage cleanup jobs ([c37b7cde](https://github.com/pixelfed/pixelfed/commit/c37b7cde))
- Added S3 command to rewrite media urls ([5b3a5610](https://github.com/pixelfed/pixelfed/commit/5b3a5610))
- Experimental home feed ([#4752](https://github.com/pixelfed/pixelfed/pull/4752)) ([c39b9afb](https://github.com/pixelfed/pixelfed/commit/c39b9afb))
- Added `app:hashtag-cached-count-update` command to update cached_count of hashtags and add to scheduler to run every 25 minutes past the hour ([1e31fee6](https://github.com/pixelfed/pixelfed/commit/1e31fee6))
- Added `app:hashtag-related-generate` command to generate related hashtags ([176b4ed7](https://github.com/pixelfed/pixelfed/commit/176b4ed7))
- Added Mutual Followers API endpoint ([33dbbe46](https://github.com/pixelfed/pixelfed/commit/33dbbe46))
- Added User Domain Blocks ([#4834](https://github.com/pixelfed/pixelfed/pull/4834)) ([fa0380ac](https://github.com/pixelfed/pixelfed/commit/fa0380ac))
- Added Parental Controls ([#4862](https://github.com/pixelfed/pixelfed/pull/4862)) ([c91f1c59](https://github.com/pixelfed/pixelfed/commit/c91f1c59))
- Added Forgot Email Feature ([67c650b1](https://github.com/pixelfed/pixelfed/commit/67c650b1))
- Added S3 IG Import Media Storage support ([#4891](https://github.com/pixelfed/pixelfed/pull/4891)) ([081360b9](https://github.com/pixelfed/pixelfed/commit/081360b9))
### Federation
- Update Privacy Settings, add support for Mastodon `indexable` search flag ([fc24630e](https://github.com/pixelfed/pixelfed/commit/fc24630e))
- Update AP Helpers, consume actor `indexable` attribute ([fbdcdd9d](https://github.com/pixelfed/pixelfed/commit/fbdcdd9d))
### Updates
- Update FollowerService, add forget method to RelationshipService call to reduce load when mass purging ([347e4f59](https://github.com/pixelfed/pixelfed/commit/347e4f59))
- Update FollowServiceWarmCache, improve handling larger following/follower lists ([61a6d904](https://github.com/pixelfed/pixelfed/commit/61a6d904))
- Update StoryApiV1Controller, add viewers route to view story viewers ([941736ce](https://github.com/pixelfed/pixelfed/commit/941736ce))
- Update NotificationService, improve cache warming query ([2496386d](https://github.com/pixelfed/pixelfed/commit/2496386d))
- Update StatusService, hydrate accounts on request instead of caching them along with status objects ([223661ec](https://github.com/pixelfed/pixelfed/commit/223661ec))
- Update profile embed, fix resize ([dc23c21d](https://github.com/pixelfed/pixelfed/commit/dc23c21d))
- Update Status model, improve thumb logic ([d969a973](https://github.com/pixelfed/pixelfed/commit/d969a973))
- Update Status model, allow unlisted thumbnails ([1f0a45b7](https://github.com/pixelfed/pixelfed/commit/1f0a45b7))
- Update StatusTagsPipeline, fix object tags and slug normalization ([d295e605](https://github.com/pixelfed/pixelfed/commit/d295e605))
- Update Note and CreateNote transformers, include attachment blurhash, width and height ([ce1afe27](https://github.com/pixelfed/pixelfed/commit/ce1afe27))
- Update ap helpers, store media attachment width and height if present ([8c969191](https://github.com/pixelfed/pixelfed/commit/8c969191))
- Update Sign-in with Mastodon, allow usage when registrations are closed ([895dc4fa](https://github.com/pixelfed/pixelfed/commit/895dc4fa))
- Update profile embeds, filter sensitive posts ([ede5ec3b](https://github.com/pixelfed/pixelfed/commit/ede5ec3b))
- Update ApiV1Controller, hydrate reblog interactions. Fixes ([#4686](https://github.com/pixelfed/pixelfed/issues/4686)) ([135798eb](https://github.com/pixelfed/pixelfed/commit/135798eb))
- Update AdminReportController, add `profile_id` to group by. Fixes ([#4685](https://github.com/pixelfed/pixelfed/issues/4685)) ([e4d3b196](https://github.com/pixelfed/pixelfed/commit/e4d3b196))
- Update user:admin command, improve logic. Fixes ([#2465](https://github.com/pixelfed/pixelfed/issues/2465)) ([01bac511](https://github.com/pixelfed/pixelfed/commit/01bac511))
- Update AP helpers, adjust RemoteAvatarFetch ttl from 24h to 3 months ([36b23fe3](https://github.com/pixelfed/pixelfed/commit/36b23fe3))
- Update AvatarPipeline, improve refresh logic and garbage collection to purge old avatars ([82798b5e](https://github.com/pixelfed/pixelfed/commit/82798b5e))
- Update CreateAvatar job, add processing constraints and set `is_remote` attribute ([319ced40](https://github.com/pixelfed/pixelfed/commit/319ced40))
- Update RemoteStatusDelete and DecrementPostCount pipelines ([edbcf3ed](https://github.com/pixelfed/pixelfed/commit/edbcf3ed))
- Update lexer regex, fix mention regex and add more tests ([778e83d3](https://github.com/pixelfed/pixelfed/commit/778e83d3))
- Update StatusTransformer, generate autolink on request ([dfe2379b](https://github.com/pixelfed/pixelfed/commit/dfe2379b))
- Update ComposeModal component, fix multi filter bug and allow media re-ordering before upload/posting ([56e315f6](https://github.com/pixelfed/pixelfed/commit/56e315f6))
- Update ApiV1Dot1Controller, allow iar rate limits to be configurable ([28a80803](https://github.com/pixelfed/pixelfed/commit/28a80803))
- Update ApiV1Dot1Controller, add domain to iar redirect ([1f82d47c](https://github.com/pixelfed/pixelfed/commit/1f82d47c))
- Update ApiV1Dot1Controller, add configurable app confirm rate limit ttl ([4c6a0719](https://github.com/pixelfed/pixelfed/commit/4c6a0719))
- Update LikePipeline, dispatch to feed queue. Fixes ([#4723](https://github.com/pixelfed/pixelfed/issues/4723)) ([da510089](https://github.com/pixelfed/pixelfed/commit/da510089))
- Update AccountImport ([5a2d7e3e](https://github.com/pixelfed/pixelfed/commit/5a2d7e3e))
- Update ImportPostController, fix IG bug with missing spaces between hashtags ([9c24157a](https://github.com/pixelfed/pixelfed/commit/9c24157a))
- Update ApiV1Controller, fix mutes in home feed ([ddc21714](https://github.com/pixelfed/pixelfed/commit/ddc21714))
- Update AP helpers, improve preferredUsername validation ([21218c79](https://github.com/pixelfed/pixelfed/commit/21218c79))
- Update delete pipelines, properly invoke StatusHashtag delete events ([ce54d29c](https://github.com/pixelfed/pixelfed/commit/ce54d29c))
- Update mail config ([0e431271](https://github.com/pixelfed/pixelfed/commit/0e431271))
- Update hashtag following ([015b1b80](https://github.com/pixelfed/pixelfed/commit/015b1b80))
- Update IncrementPostCount job, prevent overlap ([b2c9cc23](https://github.com/pixelfed/pixelfed/commit/b2c9cc23))
- Update HashtagFollowService, fix cache invalidation bug ([84f4e885](https://github.com/pixelfed/pixelfed/commit/84f4e885))
- Update Experimental Home Feed, fix remote posts, shares and reblogs ([c6a6b3ae](https://github.com/pixelfed/pixelfed/commit/c6a6b3ae))
- Update HashtagService, improve count perf ([3327a008](https://github.com/pixelfed/pixelfed/commit/3327a008))
- Update StatusHashtagService, remove problematic cache layer ([e5401f85](https://github.com/pixelfed/pixelfed/commit/e5401f85))
- Update HomeFeedPipeline, fix tag filtering ([f105f4e8](https://github.com/pixelfed/pixelfed/commit/f105f4e8))
- Update HashtagService, reduce cached_count cache ttl ([15f29f7d](https://github.com/pixelfed/pixelfed/commit/15f29f7d))
- Update ApiV1Controller, fix include_reblogs param on timelines/home endpoint, and improve limit pagination logic ([287f903b](https://github.com/pixelfed/pixelfed/commit/287f903b))
- Update StoryApiV1Controller, add self-carousel endpoint. Fixes ([#4352](https://github.com/pixelfed/pixelfed/issues/4352)) ([bcb88d5b](https://github.com/pixelfed/pixelfed/commit/bcb88d5b))
- Update FollowServiceWarmCache, use more efficient query ([fe9b4c5a](https://github.com/pixelfed/pixelfed/commit/fe9b4c5a))
- Update HomeFeedPipeline, observe mutes/blocks during fanout ([8548294c](https://github.com/pixelfed/pixelfed/commit/8548294c))
- Update FederationController, add proper following/follower counts ([3204fb96](https://github.com/pixelfed/pixelfed/commit/3204fb96))
- Update FederationController, add proper statuses counts ([3204fb96](https://github.com/pixelfed/pixelfed/commit/3204fb96))
- Update Inbox handler, fix missing object_url and uri fields for direct statuses ([a0157fce](https://github.com/pixelfed/pixelfed/commit/a0157fce))
- Update DirectMessageController, deliver direct delete activities to user inbox instead of sharedInbox ([d848792a](https://github.com/pixelfed/pixelfed/commit/d848792a))
- Update DirectMessageController, dispatch deliver and delete actions to the job queue ([7f462a80](https://github.com/pixelfed/pixelfed/commit/7f462a80))
- Update Inbox, improve story attribute collection ([06bee36c](https://github.com/pixelfed/pixelfed/commit/06bee36c))
- Update DirectMessageController, dispatch local deletes to pipeline ([98186564](https://github.com/pixelfed/pixelfed/commit/98186564))
- Update StatusPipeline, fix Direct and Story notification deletion ([4c95306f](https://github.com/pixelfed/pixelfed/commit/4c95306f))
- Update Notifications.vue, fix deprecated DM action links for story activities ([4c3823b0](https://github.com/pixelfed/pixelfed/commit/4c3823b0))
- Update ComposeModal, fix missing alttext post state ([0a068119](https://github.com/pixelfed/pixelfed/commit/0a068119))
- Update PhotoAlbumPresenter.vue, fix fullscreen mode ([822e9888](https://github.com/pixelfed/pixelfed/commit/822e9888))
- Update Timeline.vue, improve CHT pagination ([9c43e7e2](https://github.com/pixelfed/pixelfed/commit/9c43e7e2))
- Update HomeFeedPipeline, fix StatusService validation ([041c0135](https://github.com/pixelfed/pixelfed/commit/041c0135))
- Update Inbox, improve tombstone query efficiency ([759a4393](https://github.com/pixelfed/pixelfed/commit/759a4393))
- Update AccountService, add setLastActive method ([ebbd98e7](https://github.com/pixelfed/pixelfed/commit/ebbd98e7))
- Update ApiV1Controller, set last_active_at ([b6419545](https://github.com/pixelfed/pixelfed/commit/b6419545))
- Update AdminShadowFilter, fix deleted profile bug ([a492a95a](https://github.com/pixelfed/pixelfed/commit/a492a95a))
- Update FollowerService, add $silent param to remove method to more efficently purge relationships ([1664a5bc](https://github.com/pixelfed/pixelfed/commit/1664a5bc))
- Update AP ProfileTransformer, add published attribute ([adfaa2b1](https://github.com/pixelfed/pixelfed/commit/adfaa2b1))
- Update meta tags, improve descriptions and seo/og tags ([fd44c80c](https://github.com/pixelfed/pixelfed/commit/fd44c80c))
- Update login view, add email prefill logic ([d76f0168](https://github.com/pixelfed/pixelfed/commit/d76f0168))
- Update LoginController, fix captcha validation error message ([0325e171](https://github.com/pixelfed/pixelfed/commit/0325e171))
- Update ApiV1Controller, properly cast boolean sensitive parameter. Fixes #4888 ([0aff126a](https://github.com/pixelfed/pixelfed/commit/0aff126a))
- Update AccountImport.vue, fix new IG export format ([59aa6a4b](https://github.com/pixelfed/pixelfed/commit/59aa6a4b))
- Update TransformImports command, fix import service condition ([32c59f04](https://github.com/pixelfed/pixelfed/commit/32c59f04))
- Update AP helpers, more efficently update post count ([7caed381](https://github.com/pixelfed/pixelfed/commit/7caed381))
- Update AP helpers, refactor post count decrement logic ([b81ae577](https://github.com/pixelfed/pixelfed/commit/b81ae577))
- Update AP helpers, fix sensitive bug ([00ed330c](https://github.com/pixelfed/pixelfed/commit/00ed330c))
- Update NotificationEpochUpdatePipeline, use more efficient query ([4d401389](https://github.com/pixelfed/pixelfed/commit/4d401389))
- Update notification pipelines, fix non-local saving ([fa97a1f3](https://github.com/pixelfed/pixelfed/commit/fa97a1f3))
- Update NodeinfoService, disable redirects ([240e6bbe](https://github.com/pixelfed/pixelfed/commit/240e6bbe))
- Update Instance model, add entity casts ([289cad47](https://github.com/pixelfed/pixelfed/commit/289cad47))
- Update FetchNodeinfoPipeline, use more efficient dispatch ([ac01f51a](https://github.com/pixelfed/pixelfed/commit/ac01f51a))
- Update horizon.php config ([1e3acade](https://github.com/pixelfed/pixelfed/commit/1e3acade))
- Update PublicApiController, consume InstanceService blocked domains for account and statuses endpoints ([01b33fb3](https://github.com/pixelfed/pixelfed/commit/01b33fb3))
- Update ApiV1Controller, enforce blocked instance domain logic ([5b284cac](https://github.com/pixelfed/pixelfed/commit/5b284cac))
- Update ApiV2Controller, add vapid key to instance object. Thanks thisismissem! ([4d02d6f1](https://github.com/pixelfed/pixelfed/commit/4d02d6f1))
## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9)
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.8...dev)
### Added
- Import from Instagram ([#4466](https://github.com/pixelfed/pixelfed/pull/4466)) ([cf3078c5](https://github.com/pixelfed/pixelfed/commit/cf3078c5))
- Sign-in with Mastodon ([#4545](https://github.com/pixelfed/pixelfed/pull/4545)) ([45b9404e](https://github.com/pixelfed/pixelfed/commit/45b9404e))
- Health check endpoint at /api/service/health-check ([ff58f970](https://github.com/pixelfed/pixelfed/commit/ff58f970))
- Reblogs in home feed ([#4563](https://github.com/pixelfed/pixelfed/pull/4563)) ([b86d47bf](https://github.com/pixelfed/pixelfed/commit/b86d47bf))
- Account Migrations ([#4578](https://github.com/pixelfed/pixelfed/pull/4578)) ([a9220e4e](https://github.com/pixelfed/pixelfed/commit/a9220e4e))
### Updates
- Update Notifications.vue component, fix filtering logic to prevent endless spinner ([3df9b53f](https://github.com/pixelfed/pixelfed/commit/3df9b53f))
@ -306,11 +50,7 @@
- Update Timeline component, improve reblog support ([29de91e5](https://github.com/pixelfed/pixelfed/commit/29de91e5))
- Update timeline settings, add photo reblogs only option ([e2705b9a](https://github.com/pixelfed/pixelfed/commit/e2705b9a))
- Update PostContent, add text cw warning ([911504fa](https://github.com/pixelfed/pixelfed/commit/911504fa))
- Update ActivityPubFetchService, add validateUrl parameter to bypass url validation to fetch content from blocked instances ([3d1b6516](https://github.com/pixelfed/pixelfed/commit/3d1b6516))
- Update RemoteStatusDelete pipeline ([71e92261](https://github.com/pixelfed/pixelfed/commit/71e92261))
- Update RemoteStatusDelete pipeline ([fab8f25e](https://github.com/pixelfed/pixelfed/commit/fab8f25e))
- Update RemoteStatusPipeline, fix reply check ([618b6727](https://github.com/pixelfed/pixelfed/commit/618b6727))
- Update ApiV1Controller, add bookmarked to timeline entities ([ca746717](https://github.com/pixelfed/pixelfed/commit/ca746717))
- ([](https://github.com/pixelfed/pixelfed/commit/))
## [v0.11.8 (2023-05-29)](https://github.com/pixelfed/pixelfed/compare/v0.11.7...v0.11.8)

View File

@ -1,18 +0,0 @@
# See: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence,
* @dansup
# Docker related files
.editorconfig @jippi @dansup
.env @jippi @dansup
.env.* @jippi @dansup
.hadolint.yaml @jippi @dansup
.shellcheckrc @jippi @dansup
/.github/ @jippi @dansup
/docker/ @jippi @dansup
/tests/ @jippi @dansup
docker-compose.migrate.yml @jippi @dansup
docker-compose.yml @jippi @dansup
goss.yaml @jippi @dansup

View File

@ -1,361 +0,0 @@
# syntax=docker/dockerfile:1
# See https://hub.docker.com/r/docker/dockerfile
#######################################################
# Configuration
#######################################################
# See: https://github.com/mlocati/docker-php-extension-installer
ARG DOCKER_PHP_EXTENSION_INSTALLER_VERSION="2.1.80"
# See: https://github.com/composer/composer
ARG COMPOSER_VERSION="2.6"
# See: https://nginx.org/
ARG NGINX_VERSION="1.25.3"
# See: https://github.com/ddollar/forego
ARG FOREGO_VERSION="0.17.2"
# See: https://github.com/hairyhenderson/gomplate
ARG GOMPLATE_VERSION="v3.11.6"
# See: https://github.com/jippi/dottie
ARG DOTTIE_VERSION="v0.9.5"
###
# PHP base configuration
###
# See: https://hub.docker.com/_/php/tags
ARG PHP_VERSION="8.1"
# See: https://github.com/docker-library/docs/blob/master/php/README.md#image-variants
ARG PHP_BASE_TYPE="apache"
ARG PHP_DEBIAN_RELEASE="bullseye"
ARG RUNTIME_UID=33 # often called 'www-data'
ARG RUNTIME_GID=33 # often called 'www-data'
# APT extra packages
ARG APT_PACKAGES_EXTRA=
# Extensions installed via [pecl install]
# ! NOTE: imagick is installed from [master] branch on GitHub due to 8.3 bug on ARM that haven't
# ! been released yet (after +10 months)!
# ! See: https://github.com/Imagick/imagick/pull/641
ARG PHP_PECL_EXTENSIONS="redis https://codeload.github.com/Imagick/imagick/tar.gz/28f27044e435a2b203e32675e942eb8de620ee58"
ARG PHP_PECL_EXTENSIONS_EXTRA=
# Extensions installed via [docker-php-ext-install]
ARG PHP_EXTENSIONS="intl bcmath zip pcntl exif curl gd"
ARG PHP_EXTENSIONS_EXTRA=""
ARG PHP_EXTENSIONS_DATABASE="pdo_pgsql pdo_mysql pdo_sqlite"
# GPG key for nginx apt repository
ARG NGINX_GPGKEY="573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62"
# GPP key path for nginx apt repository
ARG NGINX_GPGKEY_PATH="/usr/share/keyrings/nginx-archive-keyring.gpg"
#######################################################
# Docker "copy from" images
#######################################################
# Composer docker image from Docker Hub
#
# NOTE: Docker will *not* pull this image unless it's referenced (via build target)
FROM composer:${COMPOSER_VERSION} AS composer-image
# php-extension-installer image from Docker Hub
#
# NOTE: Docker will *not* pull this image unless it's referenced (via build target)
FROM mlocati/php-extension-installer:${DOCKER_PHP_EXTENSION_INSTALLER_VERSION} AS php-extension-installer
# nginx webserver from Docker Hub.
# Used to copy some docker-entrypoint files for [nginx-runtime]
#
# NOTE: Docker will *not* pull this image unless it's referenced (via build target)
FROM nginx:${NGINX_VERSION} AS nginx-image
# Forego is a Procfile "runner" that makes it trival to run multiple
# processes under a simple init / PID 1 process.
#
# NOTE: Docker will *not* pull this image unless it's referenced (via build target)
#
# See: https://github.com/nginx-proxy/forego
FROM nginxproxy/forego:${FOREGO_VERSION}-debian AS forego-image
# Dottie makes working with .env files easier and safer
#
# NOTE: Docker will *not* pull this image unless it's referenced (via build target)
#
# See: https://github.com/jippi/dottie
FROM ghcr.io/jippi/dottie:${DOTTIE_VERSION} AS dottie-image
# gomplate-image grabs the gomplate binary from GitHub releases
#
# It's in its own layer so it can be fetched in parallel with other build steps
FROM php:${PHP_VERSION}-${PHP_BASE_TYPE}-${PHP_DEBIAN_RELEASE} AS gomplate-image
ARG TARGETARCH
ARG TARGETOS
ARG GOMPLATE_VERSION
RUN set -ex \
&& curl \
--silent \
--show-error \
--location \
--output /usr/local/bin/gomplate \
https://github.com/hairyhenderson/gomplate/releases/download/${GOMPLATE_VERSION}/gomplate_${TARGETOS}-${TARGETARCH} \
&& chmod +x /usr/local/bin/gomplate \
&& /usr/local/bin/gomplate --version
#######################################################
# Base image
#######################################################
FROM php:${PHP_VERSION}-${PHP_BASE_TYPE}-${PHP_DEBIAN_RELEASE} AS base
ARG BUILDKIT_SBOM_SCAN_STAGE="true"
ARG APT_PACKAGES_EXTRA
ARG PHP_DEBIAN_RELEASE
ARG PHP_VERSION
ARG RUNTIME_GID
ARG RUNTIME_UID
ARG TARGETPLATFORM
ENV DEBIAN_FRONTEND="noninteractive"
# Ensure we run all scripts through 'bash' rather than 'sh'
SHELL ["/bin/bash", "-c"]
# Set www-data to be RUNTIME_UID/RUNTIME_GID
RUN groupmod --gid ${RUNTIME_GID} www-data \
&& usermod --uid ${RUNTIME_UID} --gid ${RUNTIME_GID} www-data
RUN set -ex \
&& mkdir -pv /var/www/ \
&& chown -R ${RUNTIME_UID}:${RUNTIME_GID} /var/www
WORKDIR /var/www/
ENV APT_PACKAGES_EXTRA=${APT_PACKAGES_EXTRA}
# Install and configure base layer
COPY docker/shared/root/docker/install/base.sh /docker/install/base.sh
RUN --mount=type=cache,id=pixelfed-apt-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \
--mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \
/docker/install/base.sh
#######################################################
# PHP: extensions
#######################################################
FROM base AS php-extensions
ARG PHP_DEBIAN_RELEASE
ARG PHP_EXTENSIONS
ARG PHP_EXTENSIONS_DATABASE
ARG PHP_EXTENSIONS_EXTRA
ARG PHP_PECL_EXTENSIONS
ARG PHP_PECL_EXTENSIONS_EXTRA
ARG PHP_VERSION
ARG TARGETPLATFORM
COPY --from=php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
COPY docker/shared/root/docker/install/php-extensions.sh /docker/install/php-extensions.sh
RUN --mount=type=cache,id=pixelfed-pear-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/tmp/pear \
--mount=type=cache,id=pixelfed-apt-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \
--mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \
PHP_EXTENSIONS=${PHP_EXTENSIONS} \
PHP_EXTENSIONS_DATABASE=${PHP_EXTENSIONS_DATABASE} \
PHP_EXTENSIONS_EXTRA=${PHP_EXTENSIONS_EXTRA} \
PHP_PECL_EXTENSIONS=${PHP_PECL_EXTENSIONS} \
PHP_PECL_EXTENSIONS_EXTRA=${PHP_PECL_EXTENSIONS_EXTRA} \
/docker/install/php-extensions.sh
#######################################################
# Node: Build frontend
#######################################################
# NOTE: Since the nodejs build is CPU architecture agnostic,
# we only want to build once and cache it for other architectures.
# We force the (CPU) [--platform] here to be architecture
# of the "builder"/"server" and not the *target* CPU architecture
# (e.g.) building the ARM version of Pixelfed on AMD64.
FROM --platform=${BUILDARCH} node:lts AS frontend-build
ARG BUILDARCH
ARG BUILD_FRONTEND=0
ARG RUNTIME_UID
ARG NODE_ENV=production
ENV NODE_ENV=$NODE_ENV
WORKDIR /var/www/
SHELL [ "/usr/bin/bash", "-c" ]
# Install NPM dependencies
RUN --mount=type=cache,id=pixelfed-node-${BUILDARCH},sharing=locked,target=/tmp/cache \
--mount=type=bind,source=package.json,target=/var/www/package.json \
--mount=type=bind,source=package-lock.json,target=/var/www/package-lock.json \
<<EOF
if [[ $BUILD_FRONTEND -eq 1 ]];
then
npm install --cache /tmp/cache --no-save --dev
else
echo "Skipping [npm install] as --build-arg [BUILD_FRONTEND] is not set to '1'"
fi
EOF
# Copy the frontend source into the image before building
COPY --chown=${RUNTIME_UID}:${RUNTIME_GID} . /var/www
# Build the frontend with "mix" (See package.json)
RUN \
<<EOF
if [[ $BUILD_FRONTEND -eq 1 ]];
then
npm run production
else
echo "Skipping [npm run production] as --build-arg [BUILD_FRONTEND] is not set to '1'"
fi
EOF
#######################################################
# PHP: composer and source code
#######################################################
FROM php-extensions AS composer-and-src
ARG PHP_VERSION
ARG PHP_DEBIAN_RELEASE
ARG RUNTIME_UID
ARG RUNTIME_GID
ARG TARGETPLATFORM
# Make sure composer cache is targeting our cache mount later
ENV COMPOSER_CACHE_DIR="/cache/composer"
# Don't enforce any memory limits for composer
ENV COMPOSER_MEMORY_LIMIT=-1
# Disable interactvitity from composer
ENV COMPOSER_NO_INTERACTION=1
# Copy composer from https://hub.docker.com/_/composer
COPY --link --from=composer-image /usr/bin/composer /usr/bin/composer
#! Changing user to runtime user
USER ${RUNTIME_UID}:${RUNTIME_GID}
# Install composer dependencies
# NOTE: we skip the autoloader generation here since we don't have all files avaliable (yet)
RUN --mount=type=cache,id=pixelfed-composer-${PHP_VERSION},sharing=locked,target=/cache/composer \
--mount=type=bind,source=composer.json,target=/var/www/composer.json \
--mount=type=bind,source=composer.lock,target=/var/www/composer.lock \
set -ex \
&& composer install --prefer-dist --no-autoloader --ignore-platform-reqs
# Copy all other files over
COPY --chown=${RUNTIME_UID}:${RUNTIME_GID} . /var/www/
#######################################################
# Runtime: base
#######################################################
FROM php-extensions AS shared-runtime
ARG RUNTIME_GID
ARG RUNTIME_UID
ENV RUNTIME_UID=${RUNTIME_UID}
ENV RUNTIME_GID=${RUNTIME_GID}
COPY --link --from=forego-image /usr/local/bin/forego /usr/local/bin/forego
COPY --link --from=dottie-image /dottie /usr/local/bin/dottie
COPY --link --from=gomplate-image /usr/local/bin/gomplate /usr/local/bin/gomplate
COPY --link --from=composer-image /usr/bin/composer /usr/bin/composer
COPY --link --from=composer-and-src --chown=${RUNTIME_UID}:${RUNTIME_GID} /var/www /var/www
COPY --link --from=frontend-build --chown=${RUNTIME_UID}:${RUNTIME_GID} /var/www/public /var/www/public
#! Changing user to runtime user
USER ${RUNTIME_UID}:${RUNTIME_GID}
# Generate optimized autoloader now that we have all files around
RUN set -ex \
&& ENABLE_CONFIG_CACHE=false composer dump-autoload --optimize
USER root
# for detail why storage is copied this way, pls refer to https://github.com/pixelfed/pixelfed/pull/2137#discussion_r434468862
RUN set -ex \
&& cp --recursive --link --preserve=all storage storage.skel \
&& rm -rf html && ln -s public html
COPY docker/shared/root /
ENTRYPOINT ["/docker/entrypoint.sh"]
#######################################################
# Runtime: apache
#######################################################
FROM shared-runtime AS apache-runtime
COPY docker/apache/root /
RUN set -ex \
&& a2enmod rewrite remoteip proxy proxy_http \
&& a2enconf remoteip
CMD ["apache2-foreground"]
#######################################################
# Runtime: fpm
#######################################################
FROM shared-runtime AS fpm-runtime
COPY docker/fpm/root /
CMD ["php-fpm"]
#######################################################
# Runtime: nginx
#######################################################
FROM shared-runtime AS nginx-runtime
ARG NGINX_GPGKEY
ARG NGINX_GPGKEY_PATH
ARG NGINX_VERSION
ARG PHP_DEBIAN_RELEASE
ARG PHP_VERSION
ARG TARGETPLATFORM
# Install nginx dependencies
RUN --mount=type=cache,id=pixelfed-apt-lists-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt/lists \
--mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \
set -ex \
&& gpg1 --keyserver "hkp://keyserver.ubuntu.com:80" --keyserver-options timeout=10 --recv-keys "${NGINX_GPGKEY}" \
&& gpg1 --export "$NGINX_GPGKEY" > "$NGINX_GPGKEY_PATH" \
&& echo "deb [signed-by=${NGINX_GPGKEY_PATH}] https://nginx.org/packages/mainline/debian/ ${PHP_DEBIAN_RELEASE} nginx" >> /etc/apt/sources.list.d/nginx.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends nginx=${NGINX_VERSION}*
# copy docker entrypoints from the *real* nginx image directly
COPY --link --from=nginx-image /docker-entrypoint.d /docker/entrypoint.d/
COPY docker/nginx/root /
COPY docker/nginx/Procfile .
STOPSIGNAL SIGQUIT
CMD ["forego", "start", "-r"]

View File

@ -19,6 +19,7 @@ class BearerTokenResponse extends \League\OAuth2\Server\ResponseTypes\BearerToke
{
return [
'created_at' => time(),
'scope' => 'read write follow push'
];
}
}

View File

@ -1,57 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Services\AccountService;
use App\Services\Account\AccountStatService;
use App\Status;
use App\Profile;
class AccountPostCountStatUpdate extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:account-post-count-stat-update';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Update post counts from recent activities';
/**
* Execute the console command.
*/
public function handle()
{
$ids = AccountStatService::getAllPostCountIncr();
if(!$ids || !count($ids)) {
return;
}
foreach($ids as $id) {
$acct = AccountService::get($id, true);
if(!$acct) {
AccountStatService::removeFromPostCount($id);
continue;
}
$statusCount = Status::whereProfileId($id)->count();
if($statusCount != $acct['statuses_count']) {
$profile = Profile::find($id);
if(!$profile) {
AccountStatService::removeFromPostCount($id);
continue;
}
$profile->status_count = $statusCount;
$profile->save();
AccountService::del($id);
}
AccountStatService::removeFromPostCount($id);
}
return;
}
}

View File

@ -1,106 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\User;
use App\Models\DefaultDomainBlock;
use App\Models\UserDomainBlock;
use function Laravel\Prompts\text;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\progress;
class AddUserDomainBlock extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:add-user-domain-block';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Apply a domain block to all users';
/**
* Execute the console command.
*/
public function handle()
{
$domain = text('Enter domain you want to block');
$domain = strtolower($domain);
$domain = $this->validateDomain($domain);
if(!$domain || empty($domain)) {
$this->error('Invalid domain');
return;
}
$this->processBlocks($domain);
return;
}
protected function validateDomain($domain)
{
if(!strpos($domain, '.')) {
return;
}
if(str_starts_with($domain, 'https://')) {
$domain = str_replace('https://', '', $domain);
}
if(str_starts_with($domain, 'http://')) {
$domain = str_replace('http://', '', $domain);
}
$domain = strtolower(parse_url('https://' . $domain, PHP_URL_HOST));
$valid = filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME|FILTER_NULL_ON_FAILURE);
if(!$valid) {
return;
}
if($domain === config('pixelfed.domain.app')) {
$this->error('Invalid domain');
return;
}
$confirmed = confirm('Are you sure you want to block ' . $domain . '?');
if(!$confirmed) {
return;
}
return $domain;
}
protected function processBlocks($domain)
{
DefaultDomainBlock::updateOrCreate([
'domain' => $domain
]);
progress(
label: 'Updating user domain blocks...',
steps: User::lazyById(500),
callback: fn ($user) => $this->performTask($user, $domain),
);
}
protected function performTask($user, $domain)
{
if(!$user->profile_id || $user->delete_after) {
return;
}
if($user->status != null && $user->status != 'disabled') {
return;
}
UserDomainBlock::updateOrCreate([
'profile_id' => $user->profile_id,
'domain' => $domain
]);
}
}

View File

@ -82,7 +82,7 @@ class AvatarStorage extends Command
$this->line(' ');
if((bool) config_cache('pixelfed.cloud_storage')) {
if(config_cache('pixelfed.cloud_storage')) {
$this->info('✅ - Cloud storage configured');
$this->line(' ');
}
@ -92,7 +92,7 @@ class AvatarStorage extends Command
$this->line(' ');
}
if((bool) config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud')) {
if(config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud')) {
$disk = Storage::disk(config_cache('filesystems.cloud'));
$exists = $disk->exists('cache/avatars/default.jpg');
$state = $exists ? '✅' : '❌';
@ -100,7 +100,7 @@ class AvatarStorage extends Command
$this->info($msg);
}
$options = (bool) config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud') ?
$options = config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud') ?
[
'Cancel',
'Upload default avatar to cloud',
@ -164,7 +164,7 @@ class AvatarStorage extends Command
protected function uploadAvatarsToCloud()
{
if(!(bool) config_cache('pixelfed.cloud_storage') || !config('instance.avatar.local_to_cloud')) {
if(!config_cache('pixelfed.cloud_storage') || !config('instance.avatar.local_to_cloud')) {
$this->error('Enable cloud storage and avatar cloud storage to perform this action');
return;
}
@ -213,7 +213,7 @@ class AvatarStorage extends Command
return;
}
if((bool) config_cache('pixelfed.cloud_storage') == false && config_cache('federation.avatars.store_local') == false) {
if(config_cache('pixelfed.cloud_storage') == false && config_cache('federation.avatars.store_local') == false) {
$this->error('You have cloud storage disabled and local avatar storage disabled, we cannot refetch avatars.');
return;
}

View File

@ -1,115 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Cache;
use Storage;
use App\Avatar;
use App\Jobs\AvatarPipeline\AvatarStorageCleanup;
class AvatarStorageDeepClean extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'avatar:storage-deep-clean';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Cleanup avatar storage';
protected $shouldKeepRunning = true;
protected $counter = 0;
/**
* Execute the console command.
*/
public function handle(): void
{
$this->info(' ____ _ ______ __ ');
$this->info(' / __ \(_) _____ / / __/__ ____/ / ');
$this->info(' / /_/ / / |/_/ _ \/ / /_/ _ \/ __ / ');
$this->info(' / ____/ /> </ __/ / __/ __/ /_/ / ');
$this->info(' /_/ /_/_/|_|\___/_/_/ \___/\__,_/ ');
$this->info(' ');
$this->info(' Pixelfed Avatar Deep Cleaner');
$this->line(' ');
$this->info(' Purge/delete old and outdated avatars from remote accounts');
$this->line(' ');
$storage = [
'cloud' => (bool) config_cache('pixelfed.cloud_storage'),
'local' => boolval(config_cache('federation.avatars.store_local'))
];
if(!$storage['cloud'] && !$storage['local']) {
$this->error('Remote avatars are not cached locally, there is nothing to purge. Aborting...');
exit;
}
$start = 0;
if(!$this->confirm('Are you sure you want to proceed?')) {
$this->error('Aborting...');
exit;
}
if(!$this->activeCheck()) {
$this->info('Found existing deep cleaning job');
if(!$this->confirm('Do you want to continue where you left off?')) {
$this->error('Aborting...');
exit;
} else {
$start = Cache::has('cmd:asdp') ? (int) Cache::get('cmd:asdp') : (int) Storage::get('avatar-deep-clean.json');
if($start && $start < 1 || $start > PHP_INT_MAX) {
$this->error('Error fetching cached value');
$this->error('Aborting...');
exit;
}
}
}
$count = Avatar::whereNotNull('cdn_url')->where('is_remote', true)->where('id', '>', $start)->count();
$bar = $this->output->createProgressBar($count);
foreach(Avatar::whereNotNull('cdn_url')->where('is_remote', true)->where('id', '>', $start)->lazyById(10, 'id') as $avatar) {
usleep(random_int(50, 1000));
$this->counter++;
$this->handleAvatar($avatar);
$bar->advance();
}
$bar->finish();
}
protected function updateCache($id)
{
Cache::put('cmd:asdp', $id);
if($this->counter % 5 === 0) {
Storage::put('avatar-deep-clean.json', $id);
}
}
protected function activeCheck()
{
if(Storage::exists('avatar-deep-clean.json') || Cache::has('cmd:asdp')) {
return false;
}
return true;
}
protected function handleAvatar($avatar)
{
$this->updateCache($avatar->id);
$queues = ['feed', 'mmo', 'feed', 'mmo', 'feed', 'feed', 'mmo', 'low'];
$queue = $queues[random_int(0, 7)];
AvatarStorageCleanup::dispatch($avatar)->onQueue($queue);
}
}

View File

@ -35,16 +35,12 @@ class CloudMediaMigrate extends Command
*/
public function handle()
{
$enabled = (bool) config_cache('pixelfed.cloud_storage');
$enabled = config('pixelfed.cloud_storage');
if(!$enabled) {
$this->error('Cloud storage not enabled. Exiting...');
return;
}
if(!$this->confirm('Are you sure you want to proceed?')) {
return;
}
$limit = $this->option('limit');
$hugeMode = $this->option('huge');

View File

@ -1,96 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\User;
use App\Models\DefaultDomainBlock;
use App\Models\UserDomainBlock;
use function Laravel\Prompts\text;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\progress;
class DeleteUserDomainBlock extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:delete-user-domain-block';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Remove a domain block for all users';
/**
* Execute the console command.
*/
public function handle()
{
$domain = text('Enter domain you want to unblock');
$domain = strtolower($domain);
$domain = $this->validateDomain($domain);
if(!$domain || empty($domain)) {
$this->error('Invalid domain');
return;
}
$this->processUnblocks($domain);
return;
}
protected function validateDomain($domain)
{
if(!strpos($domain, '.')) {
return;
}
if(str_starts_with($domain, 'https://')) {
$domain = str_replace('https://', '', $domain);
}
if(str_starts_with($domain, 'http://')) {
$domain = str_replace('http://', '', $domain);
}
$domain = strtolower(parse_url('https://' . $domain, PHP_URL_HOST));
$valid = filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME|FILTER_NULL_ON_FAILURE);
if(!$valid) {
return;
}
if($domain === config('pixelfed.domain.app')) {
return;
}
$confirmed = confirm('Are you sure you want to unblock ' . $domain . '?');
if(!$confirmed) {
return;
}
return $domain;
}
protected function processUnblocks($domain)
{
DefaultDomainBlock::whereDomain($domain)->delete();
if(!UserDomainBlock::whereDomain($domain)->count()) {
$this->info('No results found!');
return;
}
progress(
label: 'Updating user domain blocks...',
steps: UserDomainBlock::whereDomain($domain)->lazyById(500),
callback: fn ($domainBlock) => $this->performTask($domainBlock),
);
}
protected function performTask($domainBlock)
{
$domainBlock->deleteQuietly();
}
}

View File

@ -2,11 +2,11 @@
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Media;
use Illuminate\Support\Facades\Http;
use App\Services\MediaService;
use App\Services\StatusService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
class FetchMissingMediaMimeType extends Command
{
@ -36,7 +36,7 @@ class FetchMissingMediaMimeType extends Command
continue;
}
if (! in_array($res->header('content-type'), explode(',', config_cache('pixelfed.media_types')))) {
if(!in_array($res->header('content-type'), explode(',',config('pixelfed.media_types')))) {
continue;
}

View File

@ -37,7 +37,7 @@ class FixMediaDriver extends Command
return Command::SUCCESS;
}
if((bool) config_cache('pixelfed.cloud_storage') == false) {
if(config_cache('pixelfed.cloud_storage') == false) {
$this->error('Cloud storage not enabled, exiting...');
return Command::SUCCESS;
}

View File

@ -1,57 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Hashtag;
use App\StatusHashtag;
use DB;
class HashtagCachedCountUpdate extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:hashtag-cached-count-update {--limit=100}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Update cached counter of hashtags';
/**
* Execute the console command.
*/
public function handle()
{
$limit = $this->option('limit');
$tags = Hashtag::whereNull('cached_count')->limit($limit)->get();
$count = count($tags);
if(!$count) {
return;
}
$bar = $this->output->createProgressBar($count);
$bar->start();
foreach($tags as $tag) {
$count = DB::table('status_hashtags')->whereHashtagId($tag->id)->count();
if(!$count) {
$tag->cached_count = 0;
$tag->saveQuietly();
$bar->advance();
continue;
}
$tag->cached_count = $count;
$tag->saveQuietly();
$bar->advance();
}
$bar->finish();
$this->line(' ');
return;
}
}

View File

@ -1,94 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Hashtag;
use App\StatusHashtag;
use App\Models\HashtagRelated;
use App\Services\HashtagRelatedService;
use Illuminate\Contracts\Console\PromptsForMissingInput;
use function Laravel\Prompts\multiselect;
use function Laravel\Prompts\confirm;
class HashtagRelatedGenerate extends Command implements PromptsForMissingInput
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:hashtag-related-generate {tag}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Prompt for missing input arguments using the returned questions.
*
* @return array
*/
protected function promptForMissingArgumentsUsing()
{
return [
'tag' => 'Which hashtag should we generate related tags for?',
];
}
/**
* Execute the console command.
*/
public function handle()
{
$tag = $this->argument('tag');
$hashtag = Hashtag::whereName($tag)->orWhere('slug', $tag)->first();
if(!$hashtag) {
$this->error('Hashtag not found, aborting...');
exit;
}
$exists = HashtagRelated::whereHashtagId($hashtag->id)->exists();
if($exists) {
$confirmed = confirm('Found existing related tags, do you want to regenerate them?');
if(!$confirmed) {
$this->error('Aborting...');
exit;
}
}
$this->info('Looking up #' . $tag . '...');
$tags = StatusHashtag::whereHashtagId($hashtag->id)->count();
if(!$tags || $tags < 100) {
$this->error('Not enough posts found to generate related hashtags!');
exit;
}
$this->info('Found ' . $tags . ' posts that use that hashtag');
$related = collect(HashtagRelatedService::fetchRelatedTags($tag));
$selected = multiselect(
label: 'Which tags do you want to generate?',
options: $related->pluck('name'),
required: true,
);
$filtered = $related->filter(fn($i) => in_array($i['name'], $selected))->all();
$agg_score = $related->filter(fn($i) => in_array($i['name'], $selected))->sum('related_count');
HashtagRelated::updateOrCreate([
'hashtag_id' => $hashtag->id,
], [
'related_tags' => array_values($filtered),
'agg_score' => $agg_score,
'last_calculated_at' => now()
]);
$this->info('Finished!');
}
}

View File

@ -1,118 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\CustomEmoji;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
class ImportEmojis extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'import:emojis
{path : Path to a tar.gz archive with the emojis}
{--prefix : Define a prefix for the emjoi shortcode}
{--suffix : Define a suffix for the emjoi shortcode}
{--overwrite : Overwrite existing emojis}
{--disabled : Import all emojis as disabled}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Import emojis to the database';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$path = $this->argument('path');
if (!file_exists($path) || !mime_content_type($path) == 'application/x-tar') {
$this->error('Path does not exist or is not a tarfile');
return Command::FAILURE;
}
$imported = 0;
$skipped = 0;
$failed = 0;
$tar = new \PharData($path);
$tar->decompress();
foreach (new \RecursiveIteratorIterator($tar) as $entry) {
$this->line("Processing {$entry->getFilename()}");
if (!$entry->isFile() || !$this->isImage($entry) || !$this->isEmoji($entry->getPathname())) {
$failed++;
continue;
}
$filename = pathinfo($entry->getFilename(), PATHINFO_FILENAME);
$extension = pathinfo($entry->getFilename(), PATHINFO_EXTENSION);
// Skip macOS shadow files
if (str_starts_with($filename, '._')) {
continue;
}
$shortcode = implode('', [
$this->option('prefix'),
$filename,
$this->option('suffix'),
]);
$customEmoji = CustomEmoji::whereShortcode($shortcode)->first();
if ($customEmoji && !$this->option('overwrite')) {
$skipped++;
continue;
}
$emoji = $customEmoji ?? new CustomEmoji();
$emoji->shortcode = $shortcode;
$emoji->domain = config('pixelfed.domain.app');
$emoji->disabled = $this->option('disabled');
$emoji->save();
$fileName = $emoji->id . '.' . $extension;
Storage::putFileAs('public/emoji', $entry->getPathname(), $fileName);
$emoji->media_path = 'emoji/' . $fileName;
$emoji->save();
$imported++;
Cache::forget('pf:custom_emoji');
}
$this->line("Imported: {$imported}");
$this->line("Skipped: {$skipped}");
$this->line("Failed: {$failed}");
//delete file
unlink(str_replace('.tar.gz', '.tar', $path));
return Command::SUCCESS;
}
private function isImage($file)
{
$image = getimagesize($file->getPathname());
return $image !== false;
}
private function isEmoji($filename)
{
$allowedMimeTypes = ['image/png', 'image/jpeg', 'image/webp'];
$mimeType = mime_content_type($filename);
return in_array($mimeType, $allowedMimeTypes);
}
}

View File

@ -1,54 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\ImportPost;
use App\Jobs\ImportPipeline\ImportMediaToCloudPipeline;
use function Laravel\Prompts\progress;
class ImportUploadMediaToCloudStorage extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:import-upload-media-to-cloud-storage {--limit=500}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Migrate media imported from Instagram to S3 cloud storage.';
/**
* Execute the console command.
*/
public function handle()
{
if(
(bool) config('import.instagram.storage.cloud.enabled') === false ||
(bool) config_cache('pixelfed.cloud_storage') === false
) {
$this->error('Aborted. Cloud storage is not enabled for IG imports.');
return;
}
$limit = $this->option('limit');
$progress = progress(label: 'Migrating import media', steps: $limit);
$progress->start();
$posts = ImportPost::whereUploadedToS3(false)->take($limit)->get();
foreach($posts as $post) {
ImportMediaToCloudPipeline::dispatch($post)->onQueue('low');
$progress->advance();
}
$progress->finish();
}
}

View File

@ -1,298 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Instance;
use App\Profile;
use App\Services\InstanceService;
use App\Jobs\InstancePipeline\FetchNodeinfoPipeline;
use function Laravel\Prompts\select;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\progress;
use function Laravel\Prompts\search;
use function Laravel\Prompts\table;
class InstanceManager extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:instance-manager';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Manage Instances';
/**
* Execute the console command.
*/
public function handle()
{
$action = select(
'What action do you want to perform?',
[
'Recalculate Stats',
'Ban Instance',
'Unlist Instance',
'Unlisted Instances',
'Banned Instances',
'Unban Instance',
'Relist Instance',
],
);
switch($action) {
case 'Recalculate Stats':
return $this->recalculateStats();
break;
case 'Unlisted Instances':
return $this->viewUnlistedInstances();
break;
case 'Banned Instances':
return $this->viewBannedInstances();
break;
case 'Unlist Instance':
return $this->unlistInstance();
break;
case 'Ban Instance':
return $this->banInstance();
break;
case 'Unban Instance':
return $this->unbanInstance();
break;
case 'Relist Instance':
return $this->relistInstance();
break;
}
}
protected function recalculateStats()
{
$instanceCount = Instance::count();
$confirmed = confirm('Do you want to recalculate stats for all ' . $instanceCount . ' instances?');
if(!$confirmed) {
$this->error('Aborting...');
exit;
}
$users = progress(
label: 'Updating instance stats...',
steps: Instance::all(),
callback: fn ($instance) => $this->updateInstanceStats($instance),
);
}
protected function updateInstanceStats($instance)
{
FetchNodeinfoPipeline::dispatch($instance)->onQueue('intbg');
}
protected function unlistInstance()
{
$id = search(
'Search by domain',
fn (string $value) => strlen($value) > 0
? Instance::whereUnlisted(false)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all()
: []
);
$instance = Instance::find($id);
if(!$instance) {
$this->error('Oops, an error occured');
exit;
}
$tbl = [
[
$instance->domain,
number_format($instance->status_count),
number_format($instance->user_count),
]
];
table(
['Domain', 'Status Count', 'User Count'],
$tbl
);
$confirmed = confirm('Are you sure you want to unlist this instance?');
if(!$confirmed) {
$this->error('Aborting instance unlisting');
exit;
}
$instance->unlisted = true;
$instance->save();
InstanceService::refresh();
$this->info('Successfully unlisted ' . $instance->domain . '!');
exit;
}
protected function relistInstance()
{
$id = search(
'Search by domain',
fn (string $value) => strlen($value) > 0
? Instance::whereUnlisted(true)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all()
: []
);
$instance = Instance::find($id);
if(!$instance) {
$this->error('Oops, an error occured');
exit;
}
$tbl = [
[
$instance->domain,
number_format($instance->status_count),
number_format($instance->user_count),
]
];
table(
['Domain', 'Status Count', 'User Count'],
$tbl
);
$confirmed = confirm('Are you sure you want to re-list this instance?');
if(!$confirmed) {
$this->error('Aborting instance re-listing');
exit;
}
$instance->unlisted = false;
$instance->save();
InstanceService::refresh();
$this->info('Successfully re-listed ' . $instance->domain . '!');
exit;
}
protected function banInstance()
{
$id = search(
'Search by domain',
fn (string $value) => strlen($value) > 0
? Instance::whereBanned(false)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all()
: []
);
$instance = Instance::find($id);
if(!$instance) {
$this->error('Oops, an error occured');
exit;
}
$tbl = [
[
$instance->domain,
number_format($instance->status_count),
number_format($instance->user_count),
]
];
table(
['Domain', 'Status Count', 'User Count'],
$tbl
);
$confirmed = confirm('Are you sure you want to ban this instance?');
if(!$confirmed) {
$this->error('Aborting instance ban');
exit;
}
$instance->banned = true;
$instance->save();
InstanceService::refresh();
$this->info('Successfully banned ' . $instance->domain . '!');
exit;
}
protected function unbanInstance()
{
$id = search(
'Search by domain',
fn (string $value) => strlen($value) > 0
? Instance::whereBanned(true)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all()
: []
);
$instance = Instance::find($id);
if(!$instance) {
$this->error('Oops, an error occured');
exit;
}
$tbl = [
[
$instance->domain,
number_format($instance->status_count),
number_format($instance->user_count),
]
];
table(
['Domain', 'Status Count', 'User Count'],
$tbl
);
$confirmed = confirm('Are you sure you want to unban this instance?');
if(!$confirmed) {
$this->error('Aborting instance unban');
exit;
}
$instance->banned = false;
$instance->save();
InstanceService::refresh();
$this->info('Successfully un-banned ' . $instance->domain . '!');
exit;
}
protected function viewBannedInstances()
{
$data = Instance::whereBanned(true)
->get(['domain', 'user_count', 'status_count'])
->map(function($d) {
return [
'domain' => $d->domain,
'user_count' => number_format($d->user_count),
'status_count' => number_format($d->status_count),
];
})
->toArray();
table(
['Domain', 'User Count', 'Status Count'],
$data
);
}
protected function viewUnlistedInstances()
{
$data = Instance::whereUnlisted(true)
->get(['domain', 'user_count', 'status_count', 'banned'])
->map(function($d) {
return [
'domain' => $d->domain,
'user_count' => number_format($d->user_count),
'status_count' => number_format($d->status_count),
'banned' => $d->banned ? '✅' : null
];
})
->toArray();
table(
['Domain', 'User Count', 'Status Count', 'Banned'],
$data
);
}
}

View File

@ -1,140 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Media;
use Cache, Storage;
use Illuminate\Contracts\Console\PromptsForMissingInput;
class MediaCloudUrlRewrite extends Command implements PromptsForMissingInput
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'media:cloud-url-rewrite {oldDomain} {newDomain}';
/**
* Prompt for missing input arguments using the returned questions.
*
* @return array
*/
protected function promptForMissingArgumentsUsing()
{
return [
'oldDomain' => 'The old S3 domain',
'newDomain' => 'The new S3 domain'
];
}
/**
* The console command description.
*
* @var string
*/
protected $description = 'Rewrite S3 media urls from local users';
/**
* Execute the console command.
*/
public function handle()
{
$this->preflightCheck();
$this->bootMessage();
$this->confirmCloudUrl();
}
protected function preflightCheck()
{
if(!(bool) config_cache('pixelfed.cloud_storage')) {
$this->info('Error: Cloud storage is not enabled!');
$this->error('Aborting...');
exit;
}
}
protected function bootMessage()
{
$this->info(' ____ _ ______ __ ');
$this->info(' / __ \(_) _____ / / __/__ ____/ / ');
$this->info(' / /_/ / / |/_/ _ \/ / /_/ _ \/ __ / ');
$this->info(' / ____/ /> </ __/ / __/ __/ /_/ / ');
$this->info(' /_/ /_/_/|_|\___/_/_/ \___/\__,_/ ');
$this->info(' ');
$this->info(' Media Cloud Url Rewrite Tool');
$this->info(' ===');
$this->info(' Old S3: ' . trim($this->argument('oldDomain')));
$this->info(' New S3: ' . trim($this->argument('newDomain')));
$this->info(' ');
}
protected function confirmCloudUrl()
{
$disk = Storage::disk(config('filesystems.cloud'))->url('test');
$domain = parse_url($disk, PHP_URL_HOST);
if(trim($this->argument('newDomain')) !== $domain) {
$this->error('Error: The new S3 domain you entered is not currently configured');
exit;
}
if(!$this->confirm('Confirm this is correct')) {
$this->error('Aborting...');
exit;
}
$this->updateUrls();
}
protected function updateUrls()
{
$this->info('Updating urls...');
$oldDomain = trim($this->argument('oldDomain'));
$newDomain = trim($this->argument('newDomain'));
$disk = Storage::disk(config('filesystems.cloud'));
$count = Media::whereNotNull('cdn_url')->count();
$bar = $this->output->createProgressBar($count);
$counter = 0;
$bar->start();
foreach(Media::whereNotNull('cdn_url')->lazyById(1000, 'id') as $media) {
if(strncmp($media->media_path, 'http', 4) === 0) {
$bar->advance();
continue;
}
$cdnHost = parse_url($media->cdn_url, PHP_URL_HOST);
if($oldDomain != $cdnHost || $newDomain == $cdnHost) {
$bar->advance();
continue;
}
$media->cdn_url = str_replace($oldDomain, $newDomain, $media->cdn_url);
if($media->thumbnail_url != null) {
$thumbHost = parse_url($media->thumbnail_url, PHP_URL_HOST);
if($thumbHost == $oldDomain) {
$thumbUrl = $disk->url($media->thumbnail_path);
$media->thumbnail_url = $thumbUrl;
}
}
if($media->optimized_url != null) {
$optiHost = parse_url($media->optimized_url, PHP_URL_HOST);
if($optiHost == $oldDomain) {
$optiUrl = str_replace($oldDomain, $newDomain, $media->optimized_url);
$media->optimized_url = $optiUrl;
}
}
$media->save();
$counter++;
$bar->advance();
}
$bar->finish();
$this->line(' ');
$this->info('Finished! Updated ' . $counter . ' total records!');
$this->line(' ');
$this->info('Tip: Run `php artisan cache:clear` to purge cached urls');
}
}

View File

@ -45,7 +45,7 @@ class MediaS3GarbageCollector extends Command
*/
public function handle()
{
$enabled = (bool) config_cache('pixelfed.cloud_storage');
$enabled = in_array(config_cache('pixelfed.cloud_storage'), ['1', true, 'true']);
if(!$enabled) {
$this->error('Cloud storage not enabled. Exiting...');
return;

View File

@ -1,31 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Jobs\InternalPipeline\NotificationEpochUpdatePipeline;
class NotificationEpochUpdate extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:notification-epoch-update';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Update notification epoch';
/**
* Execute the console command.
*/
public function handle()
{
NotificationEpochUpdatePipeline::dispatch();
}
}

View File

@ -1,37 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Services\Internal\SoftwareUpdateService;
use Cache;
class SoftwareUpdateRefresh extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:software-update-refresh';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Refresh latest software version data';
/**
* Execute the console command.
*/
public function handle()
{
$key = SoftwareUpdateService::cacheKey();
Cache::forget($key);
Cache::remember($key, 1209600, function() {
return SoftwareUpdateService::fetchLatest();
});
$this->info('Succesfully updated software versions!');
}
}

View File

@ -70,11 +70,6 @@ class TransformImports extends Command
}
$idk = ImportService::getId($ip->user_id, $ip->creation_year, $ip->creation_month, $ip->creation_day);
if(!$idk) {
$ip->skip_missing_media = true;
$ip->save();
continue;
}
if(Storage::exists('imports/' . $id . '/' . $ip->filename) === false) {
ImportService::clearAttempts($profile->id);

View File

@ -1,123 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Instance;
use App\Profile;
use App\Transformer\ActivityPub\Verb\DeleteActor;
use App\User;
use App\Util\ActivityPub\HttpSignature;
use GuzzleHttp\Client;
use GuzzleHttp\Pool;
use Illuminate\Console\Command;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\search;
use function Laravel\Prompts\table;
class UserAccountDelete extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:user-account-delete';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Federate Account Deletion';
/**
* Execute the console command.
*/
public function handle()
{
$id = search(
label: 'Search for the account to delete by username',
placeholder: 'john.appleseed',
options: fn (string $value) => strlen($value) > 0
? User::withTrashed()->whereStatus('deleted')->where('username', 'like', "%{$value}%")->pluck('username', 'id')->all()
: [],
);
$user = User::withTrashed()->find($id);
table(
['Username', 'Name', 'Email', 'Created'],
[[$user->username, $user->name, $user->email, $user->created_at]]
);
$confirmed = confirm(
label: 'Do you want to federate this account deletion?',
default: false,
yes: 'Proceed',
no: 'Cancel',
hint: 'This action is irreversible'
);
if (! $confirmed) {
$this->error('Aborting...');
exit;
}
$profile = Profile::withTrashed()->find($user->profile_id);
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($profile, new DeleteActor());
$activity = $fractal->createData($resource)->toArray();
$audience = Instance::whereNotNull(['shared_inbox', 'nodeinfo_last_fetched'])
->where('nodeinfo_last_fetched', '>', now()->subHours(12))
->distinct()
->pluck('shared_inbox');
$payload = json_encode($activity);
$client = new Client([
'timeout' => 10,
]);
$version = config('pixelfed.version');
$appUrl = config('app.url');
$userAgent = "(Pixelfed/{$version}; +{$appUrl})";
$requests = function ($audience) use ($client, $activity, $profile, $payload, $userAgent) {
foreach ($audience as $url) {
$headers = HttpSignature::sign($profile, $url, $activity, [
'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
'User-Agent' => $userAgent,
]);
yield function () use ($client, $url, $headers, $payload) {
return $client->postAsync($url, [
'curl' => [
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HEADER => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
],
]);
};
}
};
$pool = new Pool($client, $requests($audience), [
'concurrency' => 50,
'fulfilled' => function ($response, $index) {
},
'rejected' => function ($reason, $index) {
},
]);
$promise = $pool->promise();
$promise->wait();
}
}

View File

@ -3,17 +3,16 @@
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\PromptsForMissingInput;
use App\User;
class UserAdmin extends Command implements PromptsForMissingInput
class UserAdmin extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'user:admin {username}';
protected $signature = 'user:admin {id}';
/**
* The console command description.
@ -23,15 +22,13 @@ class UserAdmin extends Command implements PromptsForMissingInput
protected $description = 'Make a user an admin, or remove admin privileges.';
/**
* Prompt for missing input arguments using the returned questions.
* Create a new command instance.
*
* @return array
* @return void
*/
protected function promptForMissingArgumentsUsing()
public function __construct()
{
return [
'username' => 'Which username should we toggle admin privileges for?',
];
parent::__construct();
}
/**
@ -41,15 +38,16 @@ class UserAdmin extends Command implements PromptsForMissingInput
*/
public function handle()
{
$id = $this->argument('username');
$id = $this->argument('id');
if(ctype_digit($id) == true) {
$user = User::find($id);
} else {
$user = User::whereUsername($id)->first();
}
if(!$user) {
$this->error('Could not find any user with that username or id.');
exit;
}
$this->info('Found username: ' . $user->username);
$state = $user->is_admin ? 'Remove admin privileges from this user?' : 'Add admin privileges to this user?';
$confirmed = $this->confirm($state);

View File

@ -1,61 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\PromptsForMissingInput;
use App\User;
class UserToggle2FA extends Command implements PromptsForMissingInput
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'user:2fa {username}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Disable two factor authentication for given username';
/**
* Prompt for missing input arguments using the returned questions.
*
* @return array
*/
protected function promptForMissingArgumentsUsing()
{
return [
'username' => 'Which username should we disable 2FA for?',
];
}
/**
* Execute the console command.
*/
public function handle()
{
$user = User::whereUsername($this->argument('username'))->first();
if(!$user) {
$this->error('Could not find any user with that username');
exit;
}
if(!$user->{'2fa_enabled'}) {
$this->info('User did not have 2FA enabled!');
return;
}
$user->{'2fa_enabled'} = false;
$user->{'2fa_secret'} = null;
$user->{'2fa_backup_codes'} = null;
$user->save();
$this->info('Successfully disabled 2FA on this account!');
}
}

View File

@ -25,34 +25,26 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
$schedule->command('media:optimize')->hourlyAt(40)->onOneServer();
$schedule->command('media:gc')->hourlyAt(5)->onOneServer();
$schedule->command('horizon:snapshot')->everyFiveMinutes()->onOneServer();
$schedule->command('story:gc')->everyFiveMinutes()->onOneServer();
$schedule->command('gc:failedjobs')->dailyAt(3)->onOneServer();
$schedule->command('gc:passwordreset')->dailyAt('09:41')->onOneServer();
$schedule->command('gc:sessions')->twiceDaily(13, 23)->onOneServer();
$schedule->command('media:optimize')->hourlyAt(40);
$schedule->command('media:gc')->hourlyAt(5);
$schedule->command('horizon:snapshot')->everyFiveMinutes();
$schedule->command('story:gc')->everyFiveMinutes();
$schedule->command('gc:failedjobs')->dailyAt(3);
$schedule->command('gc:passwordreset')->dailyAt('09:41');
$schedule->command('gc:sessions')->twiceDaily(13, 23);
if ((bool) config_cache('pixelfed.cloud_storage') && (bool) config_cache('media.delete_local_after_cloud')) {
if(in_array(config_cache('pixelfed.cloud_storage'), ['1', true, 'true']) && config('media.delete_local_after_cloud')) {
$schedule->command('media:s3gc')->hourlyAt(15);
}
if(config('import.instagram.enabled')) {
$schedule->command('app:transform-imports')->everyTenMinutes()->onOneServer();
$schedule->command('app:import-upload-garbage-collection')->hourlyAt(51)->onOneServer();
$schedule->command('app:import-remove-deleted-accounts')->hourlyAt(37)->onOneServer();
$schedule->command('app:import-upload-clean-storage')->twiceDailyAt(1, 13, 32)->onOneServer();
if (config('import.instagram.storage.cloud.enabled') && (bool) config_cache('pixelfed.cloud_storage')) {
$schedule->command('app:import-upload-media-to-cloud-storage')->hourlyAt(39)->onOneServer();
$schedule->command('app:transform-imports')->everyFourMinutes();
$schedule->command('app:import-upload-garbage-collection')->hourlyAt(51);
$schedule->command('app:import-remove-deleted-accounts')->hourlyAt(37);
$schedule->command('app:import-upload-clean-storage')->twiceDailyAt(1, 13, 32);
}
}
$schedule->command('app:notification-epoch-update')->weeklyOn(1, '2:21')->onOneServer();
$schedule->command('app:hashtag-cached-count-update')->hourlyAt(25)->onOneServer();
$schedule->command('app:account-post-count-stat-update')->everySixHours(25)->onOneServer();
}
/**
* Register the commands for the application.
*

View File

@ -157,7 +157,7 @@ class AccountController extends Controller
$pid = $request->user()->profile_id;
$count = UserFilterService::muteCount($pid);
$maxLimit = (int) config_cache('instance.user_filters.max_user_mutes');
$maxLimit = intval(config('instance.user_filters.max_user_mutes'));
abort_if($count >= $maxLimit, 422, self::FILTER_LIMIT_MUTE_TEXT . $maxLimit . ' accounts');
if($count == 0) {
$filterCount = UserFilter::whereUserId($pid)->count();
@ -260,7 +260,7 @@ class AccountController extends Controller
]);
$pid = $request->user()->profile_id;
$count = UserFilterService::blockCount($pid);
$maxLimit = (int) config_cache('instance.user_filters.max_user_blocks');
$maxLimit = intval(config('instance.user_filters.max_user_blocks'));
abort_if($count >= $maxLimit, 422, self::FILTER_LIMIT_BLOCK_TEXT . $maxLimit . ' accounts');
if($count == 0) {
$filterCount = UserFilter::whereUserId($pid)->whereFilterType('block')->count();

View File

@ -2,20 +2,30 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\PixelfedDirectoryController;
use DB, Cache;
use App\{
DiscoverCategory,
DiscoverCategoryHashtag,
Hashtag,
Media,
Profile,
Status,
StatusHashtag,
User
};
use App\Models\ConfigCache;
use App\Services\AccountService;
use App\Services\ConfigCacheService;
use App\Services\StatusService;
use App\Status;
use App\User;
use Cache;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Validation\Rule;
use League\ISO3166\ISO3166;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use League\ISO3166\ISO3166;
use Illuminate\Support\Facades\Http;
use App\Http\Controllers\PixelfedDirectoryController;
trait AdminDirectoryController
{
@ -36,7 +46,7 @@ trait AdminDirectoryController
'uid' => (string) $user->id,
'pid' => (string) $user->profile_id,
'username' => $user->username,
'created_at' => $user->created_at,
'created_at' => $user->created_at
];
});
$config = ConfigCache::whereK('pixelfed.directory')->first();
@ -65,7 +75,6 @@ trait AdminDirectoryController
}
$res['community_guidelines'] = config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : [];
$res['curated_onboarding'] = (bool) config_cache('instance.curated_registration.enabled');
$res['open_registration'] = (bool) config_cache('pixelfed.open_registration');
$res['oauth_enabled'] = (bool) config_cache('pixelfed.oauth_enabled') && file_exists(storage_path('oauth-public.key')) && file_exists(storage_path('oauth-private.key'));
@ -74,14 +83,14 @@ trait AdminDirectoryController
$res['feature_config'] = [
'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','),
'image_quality' => config_cache('pixelfed.image_quality'),
'optimize_image' => (bool) config_cache('pixelfed.optimize_image'),
'optimize_image' => config_cache('pixelfed.optimize_image'),
'max_photo_size' => config_cache('pixelfed.max_photo_size'),
'max_caption_length' => config_cache('pixelfed.max_caption_length'),
'max_altext_length' => config_cache('pixelfed.max_altext_length'),
'enforce_account_limit' => (bool) config_cache('pixelfed.enforce_account_limit'),
'enforce_account_limit' => config_cache('pixelfed.enforce_account_limit'),
'max_account_size' => config_cache('pixelfed.max_account_size'),
'max_album_length' => config_cache('pixelfed.max_album_length'),
'account_deletion' => (bool) config_cache('pixelfed.account_deletion'),
'account_deletion' => config_cache('pixelfed.account_deletion'),
];
if(config_cache('pixelfed.directory.testimonials')) {
@ -89,7 +98,7 @@ trait AdminDirectoryController
->map(function($t) {
return [
'profile' => AccountService::get($t['profile_id']),
'body' => $t['body'],
'body' => $t['body']
];
});
$res['testimonials'] = $testimonials;
@ -110,12 +119,12 @@ trait AdminDirectoryController
'max_account_size' => 'required_if:enforce_account_limit,true|integer|min:1000000',
'max_album_length' => 'required|integer|min:4|max:20',
'account_deletion' => 'required|accepted',
'max_caption_length' => 'required|integer|min:500|max:10000',
'max_caption_length' => 'required|integer|min:500|max:10000'
]);
$res['requirements_validator'] = $validator->errors();
$res['is_eligible'] = ($res['open_registration'] || $res['curated_onboarding']) &&
$res['is_eligible'] = $res['open_registration'] &&
$res['oauth_enabled'] &&
$res['activitypub_enabled'] &&
count($res['requirements_validator']) === 0 &&
@ -150,11 +159,10 @@ trait AdminDirectoryController
$submissionState = Http::withoutVerifying()
->post('https://pixelfed.org/api/v1/directory/check-submission', [
'domain' => config('pixelfed.domain.app'),
'domain' => config('pixelfed.domain.app')
]);
$res['submission_state'] = $submissionState->json();
return $res;
}
@ -185,11 +193,11 @@ trait AdminDirectoryController
'favourite_posts' => 'array|max:12',
'favourite_posts.*' => 'distinct',
'privacy_pledge' => 'sometimes',
'banner_image' => 'sometimes|mimes:jpg,png|dimensions:width=1920,height:1080|max:5000',
'banner_image' => 'sometimes|mimes:jpg,png|dimensions:width=1920,height:1080|max:5000'
]);
$config = ConfigCache::firstOrNew([
'k' => 'pixelfed.directory',
'k' => 'pixelfed.directory'
]);
$res = $config->v ? json_decode($config->v, true) : [];
@ -212,15 +220,14 @@ trait AdminDirectoryController
$protected = [
'public/headers/.gitignore',
'public/headers/default.jpg',
'public/headers/missing.png',
'public/headers/missing.png'
];
return !in_array($name, $protected);
})
->each(function($name) {
Storage::delete($name);
});
$path = $request->file('banner_image')->storePublicly('public/headers');
$path = $request->file('banner_image')->store('public/headers');
$res['banner_image'] = $path;
ConfigCacheService::put('app.banner_image', url(Storage::url($path)));
@ -235,7 +242,6 @@ trait AdminDirectoryController
if(isset($updated['banner_image'])) {
$updated['banner_image'] = url(Storage::url($updated['banner_image']));
}
return $updated;
}
@ -243,10 +249,9 @@ trait AdminDirectoryController
{
$reqs = [];
$reqs['feature_config'] = [
'open_registration' => (bool) config_cache('pixelfed.open_registration'),
'curated_onboarding' => (bool) config_cache('instance.curated_registration.enabled'),
'open_registration' => config_cache('pixelfed.open_registration'),
'activitypub_enabled' => config_cache('federation.activitypub.enabled'),
'oauth_enabled' => (bool) config_cache('pixelfed.oauth_enabled'),
'oauth_enabled' => config_cache('pixelfed.oauth_enabled'),
'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','),
'image_quality' => config_cache('pixelfed.image_quality'),
'optimize_image' => config_cache('pixelfed.optimize_image'),
@ -260,8 +265,7 @@ trait AdminDirectoryController
];
$validator = Validator::make($reqs['feature_config'], [
'open_registration' => 'required_unless:curated_onboarding,true',
'curated_onboarding' => 'required_unless:open_registration,true',
'open_registration' => 'required|accepted',
'activitypub_enabled' => 'required|accepted',
'oauth_enabled' => 'required|accepted',
'media_types' => [
@ -278,7 +282,7 @@ trait AdminDirectoryController
'max_account_size' => 'required_if:enforce_account_limit,true|integer|min:1000000',
'max_album_length' => 'required|integer|min:4|max:20',
'account_deletion' => 'required|accepted',
'max_caption_length' => 'required|integer|min:500|max:10000',
'max_caption_length' => 'required|integer|min:500|max:10000'
]);
if(!$validator->validate()) {
@ -290,7 +294,6 @@ trait AdminDirectoryController
$data = (new PixelfedDirectoryController())->buildListing();
$res = Http::withoutVerifying()->post('https://pixelfed.org/api/v1/directory/submission', $data);
return 200;
}
@ -306,7 +309,7 @@ trait AdminDirectoryController
$protected = [
'public/headers/.gitignore',
'public/headers/default.jpg',
'public/headers/missing.png',
'public/headers/missing.png'
];
if(!$path || in_array($path, $protected)) {
return;
@ -322,7 +325,6 @@ trait AdminDirectoryController
$bannerImage->save();
Cache::forget('api:v1:instance-data-response-v1');
ConfigCacheService::put('pixelfed.directory', $directory);
return $bannerImage->v;
}
@ -352,7 +354,7 @@ trait AdminDirectoryController
public function directoryGetAddPostByIdSearch(Request $request)
{
$this->validate($request, [
'q' => 'required|integer',
'q' => 'required|integer'
]);
$id = $request->input('q');
@ -380,7 +382,6 @@ trait AdminDirectoryController
})
->values();
ConfigCacheService::put('pixelfed.directory.testimonials', $existing);
return $existing;
}
@ -388,13 +389,13 @@ trait AdminDirectoryController
{
$this->validate($request, [
'username' => 'required',
'body' => 'required|string|min:5|max:500',
'body' => 'required|string|min:5|max:500'
]);
$user = User::whereUsername($request->input('username'))->whereNull('status')->firstOrFail();
$configCache = ConfigCache::firstOrCreate([
'k' => 'pixelfed.directory.testimonials',
'k' => 'pixelfed.directory.testimonials'
]);
$testimonials = $configCache->v ? collect(json_decode($configCache->v, true)) : collect([]);
@ -405,7 +406,7 @@ trait AdminDirectoryController
$testimonials->push([
'profile_id' => (string) $user->profile_id,
'username' => $request->input('username'),
'body' => $request->input('body'),
'body' => $request->input('body')
]);
$configCache->v = json_encode($testimonials->toArray());
@ -413,9 +414,8 @@ trait AdminDirectoryController
ConfigCacheService::put('pixelfed.directory.testimonials', $configCache->v);
$res = [
'profile' => AccountService::get($user->profile_id),
'body' => $request->input('body'),
'body' => $request->input('body')
];
return $res;
}
@ -423,7 +423,7 @@ trait AdminDirectoryController
{
$this->validate($request, [
'profile_id' => 'required',
'body' => 'required|string|min:5|max:500',
'body' => 'required|string|min:5|max:500'
]);
$profile_id = $request->input('profile_id');
@ -431,7 +431,7 @@ trait AdminDirectoryController
$user = User::whereProfileId($profile_id)->firstOrFail();
$configCache = ConfigCache::firstOrCreate([
'k' => 'pixelfed.directory.testimonials',
'k' => 'pixelfed.directory.testimonials'
]);
$testimonials = $configCache->v ? collect(json_decode($configCache->v, true)) : collect([]);
@ -440,7 +440,6 @@ trait AdminDirectoryController
if($t['profile_id'] == $profile_id) {
$t['body'] = $body;
}
return $t;
})
->values();

View File

@ -2,34 +2,38 @@
namespace App\Http\Controllers\Admin;
use App\AccountInterstitial;
use App\Http\Resources\AdminReport;
use App\Http\Resources\AdminRemoteReport;
use App\Http\Resources\AdminSpamReport;
use App\Jobs\DeletePipeline\DeleteAccountPipeline;
use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline;
use App\Jobs\StatusPipeline\RemoteStatusDelete;
use App\Jobs\StatusPipeline\StatusDelete;
use App\Jobs\StoryPipeline\StoryDelete;
use App\Notification;
use App\Profile;
use App\Report;
use App\Models\RemoteReport;
use App\Services\AccountService;
use App\Services\ModLogService;
use App\Services\NetworkTimelineService;
use App\Services\NotificationService;
use App\Services\PublicTimelineService;
use App\Services\StatusService;
use App\Status;
use App\Story;
use App\User;
use Cache;
use Storage;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;
use App\Services\AccountService;
use App\Services\StatusService;
use App\{
AccountInterstitial,
Contact,
Hashtag,
Newsroom,
Notification,
OauthClient,
Profile,
Report,
Status,
Story,
User
};
use Illuminate\Validation\Rule;
use App\Services\StoryService;
use App\Services\ModLogService;
use App\Jobs\DeletePipeline\DeleteAccountPipeline;
use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline;
use App\Jobs\DeletePipeline\DeleteRemoteStatusPipeline;
use App\Jobs\StatusPipeline\StatusDelete;
use App\Http\Resources\AdminReport;
use App\Http\Resources\AdminSpamReport;
use App\Services\NotificationService;
use App\Services\PublicTimelineService;
use App\Services\NetworkTimelineService;
trait AdminReportController
{
@ -49,7 +53,7 @@ trait AdminReportController
$mailVerifications = Redis::scard('email:manual');
if($filter == 'open' && $page == 1) {
$reports = Cache::remember('admin-dash:reports:list-cache', 300, function () use ($filter) {
$reports = Cache::remember('admin-dash:reports:list-cache', 300, function() use($page, $filter) {
return Report::whereHas('status')
->whereHas('reportedUser')
->whereHas('reporter')
@ -83,7 +87,6 @@ trait AdminReportController
if($request->has('ref') && $request->input('ref') == 'email') {
return redirect('/i/admin/reports?tab=report&id=' . $report->id);
}
return view('admin.reports.show', compact('report'));
}
@ -93,7 +96,6 @@ trait AdminReportController
->whereNull('appeal_handled_at')
->latest()
->paginate(6);
return view('admin.reports.appeals', compact('appeals'));
}
@ -103,14 +105,13 @@ trait AdminReportController
->whereNull('appeal_handled_at')
->findOrFail($id);
$meta = json_decode($appeal->meta);
return view('admin.reports.show_appeal', compact('appeal', 'meta'));
}
public function spam(Request $request)
{
$this->validate($request, [
'tab' => 'sometimes|in:home,not-spam,spam,settings,custom,exemptions',
'tab' => 'sometimes|in:home,not-spam,spam,settings,custom,exemptions'
]);
$tab = $request->input('tab', 'home');
@ -142,7 +143,6 @@ trait AdminReportController
if(config('database.default') != 'mysql') {
return 0;
}
return AccountInterstitial::selectRaw('*, count(id) as counter')
->whereType('post.autospam')
->groupBy('user_id')
@ -152,11 +152,11 @@ trait AdminReportController
$avgOpen = Cache::remember('admin-dash:reports:spam-count:avgopen', 43200, function() {
if(config('database.default') != 'mysql') {
return '0';
return "0";
}
$seconds = AccountInterstitial::selectRaw('DATE(created_at) AS start_date, AVG(TIME_TO_SEC(TIMEDIFF(appeal_handled_at, created_at))) AS timediff')->whereType('post.autospam')->whereNotNull('appeal_handled_at')->where('created_at', '>', now()->subMonth())->get();
if(!$seconds) {
return '0';
return "0";
}
$mins = floor($seconds->avg('timediff') / 60);
@ -170,7 +170,7 @@ trait AdminReportController
return floor($mins / 60 / 24) . ' day(s)';
});
$avgCount = $totalCount && $avg ? floor($totalCount / $avg) : '0';
$avgCount = $totalCount && $avg ? floor($totalCount / $avg) : "0";
if(in_array($tab, ['home', 'spam', 'not-spam'])) {
$appeals = AccountInterstitial::whereType('post.autospam')
@ -194,20 +194,18 @@ trait AdminReportController
$appeals = $appeals->appends(['tab' => $tab]);
}
} else {
$appeals = new class
{
public function count()
{
$appeals = new class {
public function count() {
return 0;
}
public function render()
{
public function render() {
return;
}
};
}
return view('admin.reports.spam', compact('tab', 'appeals', 'openCount', 'monthlyCount', 'totalCount', 'avgCount', 'avgOpen', 'uncategorized'));
}
@ -219,7 +217,6 @@ trait AdminReportController
return redirect('/i/admin/reports?tab=autospam&id=' . $appeal->id);
}
$meta = json_decode($appeal->meta);
return view('admin.reports.show_spam', compact('appeal', 'meta'));
}
@ -258,14 +255,13 @@ trait AdminReportController
});
Cache::forget('admin-dash:reports:spam-sync');
return redirect('/i/admin/reports/autospam');
}
public function updateSpam(Request $request, $id)
{
$this->validate($request, [
'action' => 'required|in:dismiss,approve,dismiss-all,approve-all,delete-account,mark-spammer',
'action' => 'required|in:dismiss,approve,dismiss-all,approve-all,delete-account,mark-spammer'
]);
$action = $request->input('action');
@ -311,7 +307,6 @@ trait AdminReportController
Cache::forget('profiles:private');
DeleteAccountPipeline::dispatch($user);
return;
}
@ -323,7 +318,6 @@ trait AdminReportController
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
Cache::forget('admin-dash:reports:spam-count');
return $res;
}
@ -336,7 +330,6 @@ trait AdminReportController
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
Cache::forget('admin-dash:reports:spam-count');
return $res;
}
@ -362,7 +355,6 @@ trait AdminReportController
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
Cache::forget('admin-dash:reports:spam-count');
return $res;
}
@ -378,7 +370,7 @@ trait AdminReportController
$pro->update([
'unlisted' => true,
'cw' => true,
'no_autolink' => true,
'no_autolink' => true
]);
Status::whereProfileId($pro->id)
@ -394,7 +386,6 @@ trait AdminReportController
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
Cache::forget('admin-dash:reports:spam-count');
return $res;
}
@ -420,7 +411,7 @@ trait AdminReportController
public function updateAppeal(Request $request, $id)
{
$this->validate($request, [
'action' => 'required|in:dismiss,approve',
'action' => 'required|in:dismiss,approve'
]);
$action = $request->input('action');
@ -432,7 +423,6 @@ trait AdminReportController
$appeal->appeal_handled_at = now();
$appeal->save();
Cache::forget('admin-dash:reports:ai-count');
return redirect('/i/admin/reports/appeals');
}
@ -451,7 +441,7 @@ trait AdminReportController
break;
default:
// code...
# code...
break;
}
@ -551,7 +541,7 @@ trait AdminReportController
'3' => 'unlist',
'4' => 'delete',
'5' => 'shadowban',
'6' => 'ban',
'6' => 'ban'
];
}
@ -559,7 +549,7 @@ trait AdminReportController
{
$this->validate($request, [
'action' => 'required|integer|min:1|max:10',
'ids' => 'required|array',
'ids' => 'required|array'
]);
$action = $this->actionMap()[$request->input('action')];
$ids = $request->input('ids');
@ -569,9 +559,8 @@ trait AdminReportController
}
$res = [
'message' => 'Success',
'code' => 200,
'code' => 200
];
return response()->json($res);
}
@ -595,7 +584,6 @@ trait AdminReportController
return [];
}
$account['email'] = $user->email;
return $account;
})
->filter(function($res) {
@ -603,7 +591,6 @@ trait AdminReportController
})
->values();
}
return view('admin.reports.mail_verification', compact('reports', 'ignored'));
}
@ -611,7 +598,6 @@ trait AdminReportController
{
$id = $request->input('id');
Redis::sadd('email:manual-ignored', $id);
return redirect('/i/admin/reports');
}
@ -623,14 +609,12 @@ trait AdminReportController
Redis::srem('email:manual-ignored', $id);
$user->email_verified_at = now();
$user->save();
return redirect('/i/admin/reports');
}
public function reportMailVerifyClearIgnored(Request $request)
{
Redis::del('email:manual-ignored');
return [200];
}
@ -643,10 +627,8 @@ trait AdminReportController
'autospam' => AccountInterstitial::whereType('post.autospam')->count(),
'autospam_open' => AccountInterstitial::whereType('post.autospam')->whereNull(['appeal_handled_at'])->count(),
'appeals' => AccountInterstitial::whereNotNull('appeal_requested_at')->whereNull('appeal_handled_at')->count(),
'remote_open' => RemoteReport::whereNull('action_taken_at')->count(),
'email_verification_requests' => Redis::scard('email:manual'),
'email_verification_requests' => Redis::scard('email:manual')
];
return $stats;
}
@ -661,25 +643,7 @@ trait AdminReportController
$q->whereNull('admin_seen') :
$q->whereNotNull('admin_seen');
})
->groupBy(['id', 'object_id', 'object_type', 'profile_id'])
->cursorPaginate(6)
->withQueryString()
);
return $reports;
}
public function reportsApiRemote(Request $request)
{
$filter = $request->input('filter') == 'closed' ? 'closed' : 'open';
$reports = AdminRemoteReport::collection(
RemoteReport::orderBy('id', 'desc')
->when($filter, function ($q, $filter) {
return $filter == 'open' ?
$q->whereNull('action_taken_at') :
$q->whereNotNull('action_taken_at');
})
->groupBy(['object_id', 'object_type'])
->cursorPaginate(6)
->withQueryString()
);
@ -690,7 +654,6 @@ trait AdminReportController
public function reportsApiGet(Request $request, $id)
{
$report = Report::findOrFail($id);
return new AdminReport($report);
}
@ -700,8 +663,8 @@ trait AdminReportController
'object_id' => 'required',
'object_type' => 'required',
'id' => 'required',
'action' => 'required|in:ignore,nsfw,unlist,private,delete,delete-all',
'action_type' => 'required|in:post,profile,story',
'action' => 'required|in:ignore,nsfw,unlist,private,delete',
'action_type' => 'required|in:post,profile'
]);
$report = Report::whereObjectId($request->input('object_id'))->findOrFail($request->input('id'));
@ -710,92 +673,11 @@ trait AdminReportController
return $this->reportsHandleProfileAction($report, $request->input('action'));
} else if($request->input('action_type') === 'post') {
return $this->reportsHandleStatusAction($report, $request->input('action'));
} elseif ($request->input('action_type') === 'story') {
return $this->reportsHandleStoryAction($report, $request->input('action'));
}
return $report;
}
protected function reportsHandleStoryAction($report, $action)
{
switch ($action) {
case 'ignore':
Report::whereObjectId($report->object_id)
->whereObjectType($report->object_type)
->update([
'admin_seen' => now(),
]);
return [200];
break;
case 'delete':
$profile = Profile::find($report->reported_profile_id);
$story = Story::whereProfileId($profile->id)->find($report->object_id);
abort_if(! $story, 400, 'Invalid or missing story');
$story->active = false;
$story->save();
ModLogService::boot()
->objectUid($profile->id)
->objectId($report->object_id)
->objectType('App\Story::class')
->user(request()->user())
->action('admin.user.moderate')
->metadata([
'action' => 'delete',
'message' => 'Success!',
])
->accessLevel('admin')
->save();
Report::whereObjectId($report->object_id)
->whereObjectType($report->object_type)
->update([
'admin_seen' => now(),
]);
StoryDelete::dispatch($story)->onQueue('story');
return [200];
break;
case 'delete-all':
$profile = Profile::find($report->reported_profile_id);
$stories = Story::whereProfileId($profile->id)->whereActive(true)->get();
abort_if(! $stories || ! $stories->count(), 400, 'Invalid or missing stories');
ModLogService::boot()
->objectUid($profile->id)
->objectId($report->object_id)
->objectType('App\Story::class')
->user(request()->user())
->action('admin.user.moderate')
->metadata([
'action' => 'delete-all',
'message' => 'Success!',
])
->accessLevel('admin')
->save();
Report::where('reported_profile_id', $profile->id)
->whereObjectType('App\Story')
->whereNull('admin_seen')
->update([
'admin_seen' => now(),
]);
$stories->each(function ($story) {
StoryDelete::dispatch($story)->onQueue('story');
});
return [200];
break;
}
}
protected function reportsHandleProfileAction($report, $action)
{
switch($action) {
@ -803,9 +685,8 @@ trait AdminReportController
Report::whereObjectId($report->object_id)
->whereObjectType($report->object_type)
->update([
'admin_seen' => now(),
'admin_seen' => now()
]);
return [200];
break;
@ -844,7 +725,7 @@ trait AdminReportController
->action('admin.user.moderate')
->metadata([
'action' => 'cw',
'message' => 'Success!',
'message' => 'Success!'
])
->accessLevel('admin')
->save();
@ -853,9 +734,8 @@ trait AdminReportController
->whereObjectType($report->object_type)
->update([
'nsfw' => true,
'admin_seen' => now(),
'admin_seen' => now()
]);
return [200];
break;
@ -895,7 +775,7 @@ trait AdminReportController
->action('admin.user.moderate')
->metadata([
'action' => 'unlisted',
'message' => 'Success!',
'message' => 'Success!'
])
->accessLevel('admin')
->save();
@ -903,9 +783,8 @@ trait AdminReportController
Report::whereObjectId($report->object_id)
->whereObjectType($report->object_type)
->update([
'admin_seen' => now(),
'admin_seen' => now()
]);
return [200];
break;
@ -945,7 +824,7 @@ trait AdminReportController
->action('admin.user.moderate')
->metadata([
'action' => 'private',
'message' => 'Success!',
'message' => 'Success!'
])
->accessLevel('admin')
->save();
@ -953,9 +832,8 @@ trait AdminReportController
Report::whereObjectId($report->object_id)
->whereObjectType($report->object_type)
->update([
'admin_seen' => now(),
'admin_seen' => now()
]);
return [200];
break;
@ -1006,7 +884,7 @@ trait AdminReportController
Report::whereObjectId($report->object_id)
->whereObjectType($report->object_type)
->update([
'admin_seen' => now(),
'admin_seen' => now()
]);
if($profile->user_id) {
@ -1028,7 +906,6 @@ trait AdminReportController
AccountService::del($profile->id);
DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('high');
}
return [200];
break;
}
@ -1041,9 +918,8 @@ trait AdminReportController
Report::whereObjectId($report->object_id)
->whereObjectType($report->object_type)
->update([
'admin_seen' => now(),
'admin_seen' => now()
]);
return [200];
break;
@ -1067,7 +943,7 @@ trait AdminReportController
->action('admin.status.moderate')
->metadata([
'action' => 'cw',
'message' => 'Success!',
'message' => 'Success!'
])
->accessLevel('admin')
->save();
@ -1076,9 +952,8 @@ trait AdminReportController
->whereObjectType($report->object_type)
->update([
'nsfw' => true,
'admin_seen' => now(),
'admin_seen' => now()
]);
return [200];
break;
@ -1105,7 +980,7 @@ trait AdminReportController
->action('admin.status.moderate')
->metadata([
'action' => 'private',
'message' => 'Success!',
'message' => 'Success!'
])
->accessLevel('admin')
->save();
@ -1113,9 +988,8 @@ trait AdminReportController
Report::whereObjectId($report->object_id)
->whereObjectType($report->object_type)
->update([
'admin_seen' => now(),
'admin_seen' => now()
]);
return [200];
break;
@ -1144,7 +1018,7 @@ trait AdminReportController
->action('admin.status.moderate')
->metadata([
'action' => 'unlist',
'message' => 'Success!',
'message' => 'Success!'
])
->accessLevel('admin')
->save();
@ -1152,9 +1026,8 @@ trait AdminReportController
Report::whereObjectId($report->object_id)
->whereObjectType($report->object_type)
->update([
'admin_seen' => now(),
'admin_seen' => now()
]);
return [200];
break;
@ -1176,13 +1049,13 @@ trait AdminReportController
StatusDelete::dispatch($status)->onQueue('high');
} else {
NetworkTimelineService::del($status->id);
RemoteStatusDelete::dispatch($status)->onQueue('high');
DeleteRemoteStatusPipeline::dispatch($status)->onQueue('high');
}
Report::whereObjectId($report->object_id)
->whereObjectType($report->object_type)
->update([
'admin_seen' => now(),
'admin_seen' => now()
]);
return [200];
@ -1230,7 +1103,6 @@ trait AdminReportController
Cache::forget('admin-dash:reports:spam-count');
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $report->user->profile_id);
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $report->user->profile_id);
return [$action, $report];
}
@ -1278,7 +1150,7 @@ trait AdminReportController
->whereUserId($appeal->user_id)
->update([
'appeal_handled_at' => now(),
'is_spam' => true,
'is_spam' => true
]);
}
@ -1346,176 +1218,6 @@ trait AdminReportController
public function reportsApiSpamGet(Request $request, $id)
{
$report = AccountInterstitial::findOrFail($id);
return new AdminSpamReport($report);
}
public function reportsApiRemoteHandle(Request $request)
{
$this->validate($request, [
'id' => 'required|exists:remote_reports,id',
'action' => 'required|in:mark-read,cw-posts,unlist-posts,delete-posts,private-posts,mark-all-read-by-domain,mark-all-read-by-username,cw-all-posts,private-all-posts,unlist-all-posts'
]);
$report = RemoteReport::findOrFail($request->input('id'));
$user = User::whereProfileId($report->account_id)->first();
$ogPublicStatuses = [];
$ogUnlistedStatuses = [];
$ogNonCwStatuses = [];
switch ($request->input('action')) {
case 'mark-read':
$report->action_taken_at = now();
$report->save();
break;
case 'mark-all-read-by-domain':
RemoteReport::whereInstanceId($report->instance_id)->update(['action_taken_at' => now()]);
break;
case 'cw-posts':
$statuses = Status::find($report->status_ids);
foreach($statuses as $status) {
if($report->account_id != $status->profile_id) {
continue;
}
if(!$status->is_nsfw) {
$ogNonCwStatuses[] = $status->id;
}
$status->is_nsfw = true;
$status->saveQuietly();
StatusService::del($status->id);
}
$report->action_taken_at = now();
$report->save();
break;
case 'cw-all-posts':
foreach(Status::whereProfileId($report->account_id)->lazyById(50, 'id') as $status) {
if($status->is_nsfw || $status->reblog_of_id) {
continue;
}
if(!$status->is_nsfw) {
$ogNonCwStatuses[] = $status->id;
}
$status->is_nsfw = true;
$status->saveQuietly();
StatusService::del($status->id);
}
break;
case 'unlist-posts':
$statuses = Status::find($report->status_ids);
foreach($statuses as $status) {
if($report->account_id != $status->profile_id) {
continue;
}
if($status->scope === 'public') {
$ogPublicStatuses[] = $status->id;
$status->scope = 'unlisted';
$status->visibility = 'unlisted';
$status->saveQuietly();
StatusService::del($status->id);
}
}
$report->action_taken_at = now();
$report->save();
break;
case 'unlist-all-posts':
foreach(Status::whereProfileId($report->account_id)->lazyById(50, 'id') as $status) {
if($status->visibility !== 'public' || $status->reblog_of_id) {
continue;
}
$ogPublicStatuses[] = $status->id;
$status->visibility = 'unlisted';
$status->scope = 'unlisted';
$status->saveQuietly();
StatusService::del($status->id);
}
break;
case 'private-posts':
$statuses = Status::find($report->status_ids);
foreach($statuses as $status) {
if($report->account_id != $status->profile_id) {
continue;
}
if(in_array($status->scope, ['public', 'unlisted', 'private'])) {
if($status->scope === 'public') {
$ogPublicStatuses[] = $status->id;
}
$status->scope = 'private';
$status->visibility = 'private';
$status->saveQuietly();
StatusService::del($status->id);
}
}
$report->action_taken_at = now();
$report->save();
break;
case 'private-all-posts':
foreach(Status::whereProfileId($report->account_id)->lazyById(50, 'id') as $status) {
if(!in_array($status->visibility, ['public', 'unlisted']) || $status->reblog_of_id) {
continue;
}
if($status->visibility === 'public') {
$ogPublicStatuses[] = $status->id;
} else if($status->visibility === 'unlisted') {
$ogUnlistedStatuses[] = $status->id;
}
$status->visibility = 'private';
$status->scope = 'private';
$status->saveQuietly();
StatusService::del($status->id);
}
break;
case 'delete-posts':
$statuses = Status::find($report->status_ids);
foreach($statuses as $status) {
if($report->account_id != $status->profile_id) {
continue;
}
StatusDelete::dispatch($status);
}
$report->action_taken_at = now();
$report->save();
break;
case 'mark-all-read-by-username':
RemoteReport::whereNull('action_taken_at')->whereAccountId($report->account_id)->update(['action_taken_at' => now()]);
break;
default:
abort(404);
break;
}
if($ogPublicStatuses && count($ogPublicStatuses)) {
Storage::disk('local')->put('mod-log-cache/' . $report->account_id . '/' . now()->format('Y-m-d') . '-og-public-statuses.json', json_encode($ogPublicStatuses, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
}
if($ogNonCwStatuses && count($ogNonCwStatuses)) {
Storage::disk('local')->put('mod-log-cache/' . $report->account_id . '/' . now()->format('Y-m-d') . '-og-noncw-statuses.json', json_encode($ogNonCwStatuses, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
}
if($ogUnlistedStatuses && count($ogUnlistedStatuses)) {
Storage::disk('local')->put('mod-log-cache/' . $report->account_id . '/' . now()->format('Y-m-d') . '-og-unlisted-statuses.json', json_encode($ogUnlistedStatuses, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
}
ModLogService::boot()
->user(request()->user())
->objectUid($user ? $user->id : null)
->objectId($report->id)
->objectType('App\Report::class')
->action('admin.report.moderate')
->metadata([
'action' => $request->input('action'),
'duration_active' => now()->parse($report->created_at)->diffForHumans()
])
->accessLevel('admin')
->save();
if($report->status_ids) {
foreach($report->status_ids as $sid) {
RemoteReport::whereNull('action_taken_at')
->whereJsonContains('status_ids', [$sid])
->update(['action_taken_at' => now()]);
}
}
return [200];
}
}

View File

@ -2,20 +2,18 @@
namespace App\Http\Controllers\Admin;
use App\Models\ConfigCache;
use App\Models\InstanceActor;
use App\Page;
use App\Profile;
use App\Services\AccountService;
use App\Services\AdminSettingsService;
use App\Services\ConfigCacheService;
use App\Services\FilesystemService;
use App\User;
use App\Util\Site\Config;
use Artisan;
use Cache;
use DB;
use Artisan, Cache, DB;
use Illuminate\Http\Request;
use Carbon\Carbon;
use App\{Comment, Like, Media, Page, Profile, Report, Status, User};
use App\Models\InstanceActor;
use App\Http\Controllers\Controller;
use App\Util\Lexer\PrettyNumber;
use App\Models\ConfigCache;
use App\Services\AccountService;
use App\Services\ConfigCacheService;
use App\Util\Site\Config;
use Illuminate\Support\Str;
trait AdminSettingsController
{
@ -34,10 +32,13 @@ trait AdminSettingsController
$availableAdmins = User::whereIsAdmin(true)->get();
$currentAdmin = config_cache('instance.admin.pid') ? AccountService::get(config_cache('instance.admin.pid'), true) : null;
$openReg = (bool) config_cache('pixelfed.open_registration');
$curOnboarding = (bool) config_cache('instance.curated_registration.enabled');
$regState = $openReg ? 'open' : ($curOnboarding ? 'filtered' : 'closed');
$accountMigration = (bool) config_cache('federation.migration');
// $system = [
// 'permissions' => is_writable(base_path('storage')) && is_writable(base_path('bootstrap')),
// 'max_upload_size' => ini_get('post_max_size'),
// 'image_driver' => config('image.driver'),
// 'image_driver_loaded' => extension_loaded(config('image.driver'))
// ];
return view('admin.settings.home', compact(
'jpeg',
@ -50,9 +51,8 @@ trait AdminSettingsController
'cloud_disk',
'cloud_ready',
'availableAdmins',
'currentAdmin',
'regState',
'accountMigration'
'currentAdmin'
// 'system'
));
}
@ -71,33 +71,8 @@ trait AdminSettingsController
'type_mp4' => 'nullable',
'type_webp' => 'nullable',
'admin_account_id' => 'nullable',
'regs' => 'required|in:open,filtered,closed',
'account_migration' => 'nullable',
'rule_delete' => 'sometimes',
]);
$orb = false;
$cob = false;
switch ($request->input('regs')) {
case 'open':
$orb = true;
$cob = false;
break;
case 'filtered':
$orb = false;
$cob = true;
break;
case 'closed':
$orb = false;
$cob = false;
break;
}
ConfigCacheService::put('pixelfed.open_registration', (bool) $orb);
ConfigCacheService::put('instance.curated_registration.enabled', (bool) $cob);
if($request->filled('admin_account_id')) {
ConfigCacheService::put('instance.admin.pid', $request->admin_account_id);
Cache::forget('api:v1:instance-data:contact');
@ -115,7 +90,6 @@ trait AdminSettingsController
ConfigCacheService::put('app.rules', $json);
Cache::forget('api:v1:instance-data:rules');
Cache::forget('api:v1:instance-data-response-v1');
return 200;
}
@ -154,7 +128,7 @@ trait AdminSettingsController
'account_limit' => 'pixelfed.max_account_size',
'custom_css' => 'uikit.custom.css',
'custom_js' => 'uikit.custom.js',
'about_title' => 'about.title',
'about_title' => 'about.title'
];
foreach ($keys as $key => $value) {
@ -169,7 +143,7 @@ trait AdminSettingsController
$bools = [
'activitypub' => 'federation.activitypub.enabled',
// 'open_registration' => 'pixelfed.open_registration',
'open_registration' => 'pixelfed.open_registration',
'mobile_apis' => 'pixelfed.oauth_enabled',
'stories' => 'instance.stories.enabled',
'ig_import' => 'pixelfed.import.instagram.enabled',
@ -182,7 +156,6 @@ trait AdminSettingsController
'account_autofollow' => 'account.autofollow',
'show_directory' => 'instance.landing.show_directory',
'show_explore_feed' => 'instance.landing.show_explore',
'account_migration' => 'federation.migration',
];
foreach ($bools as $key => $value) {
@ -244,7 +217,6 @@ trait AdminSettingsController
{
$path = storage_path('app/'.config('app.name'));
$files = is_dir($path) ? new \DirectoryIterator($path) : [];
return view('admin.settings.backups', compact('files'));
}
@ -256,7 +228,6 @@ trait AdminSettingsController
public function settingsStorage(Request $request)
{
$storage = [];
return view('admin.settings.storage', compact('storage'));
}
@ -268,7 +239,6 @@ trait AdminSettingsController
public function settingsPages(Request $request)
{
$pages = Page::orderByDesc('updated_at')->paginate(10);
return view('admin.pages.home', compact('pages'));
}
@ -290,7 +260,7 @@ trait AdminSettingsController
$expQuery = $exp->getValue(DB::connection()->getQueryGrammar());
$sys['database'] = [
'name' => 'Postgres',
'version' => explode(' ', DB::select($expQuery)[0]->version)[1],
'version' => explode(' ', DB::select($expQuery)[0]->version)[1]
];
break;
@ -299,587 +269,17 @@ trait AdminSettingsController
$expQuery = $exp->getValue(DB::connection()->getQueryGrammar());
$sys['database'] = [
'name' => 'MySQL',
'version' => DB::select($expQuery)[0]->{'version()'},
'version' => DB::select($expQuery)[0]->{'version()'}
];
break;
default:
$sys['database'] = [
'name' => 'Unknown',
'version' => '?',
'version' => '?'
];
break;
}
return view('admin.settings.system', compact('sys'));
}
public function settingsApiFetch(Request $request)
{
$cloud_storage = ConfigCacheService::get('pixelfed.cloud_storage');
$cloud_disk = config('filesystems.cloud');
$cloud_ready = ! empty(config('filesystems.disks.'.$cloud_disk.'.key')) && ! empty(config('filesystems.disks.'.$cloud_disk.'.secret'));
$types = explode(',', ConfigCacheService::get('pixelfed.media_types'));
$rules = ConfigCacheService::get('app.rules') ? json_decode(ConfigCacheService::get('app.rules'), true) : [];
$jpeg = in_array('image/jpg', $types) || in_array('image/jpeg', $types);
$png = in_array('image/png', $types);
$gif = in_array('image/gif', $types);
$mp4 = in_array('video/mp4', $types);
$webp = in_array('image/webp', $types);
$availableAdmins = User::whereIsAdmin(true)->get();
$currentAdmin = config_cache('instance.admin.pid') ? AccountService::get(config_cache('instance.admin.pid'), true) : null;
$openReg = (bool) config_cache('pixelfed.open_registration');
$curOnboarding = (bool) config_cache('instance.curated_registration.enabled');
$regState = $openReg ? 'open' : ($curOnboarding ? 'filtered' : 'closed');
$accountMigration = (bool) config_cache('federation.migration');
$autoFollow = config_cache('account.autofollow_usernames');
if (strlen($autoFollow) > 3) {
$autoFollow = explode(',', $autoFollow);
}
$res = AdminSettingsService::getAll();
return response()->json($res);
}
public function settingsApiRulesAdd(Request $request)
{
$this->validate($request, [
'rule' => 'required|string|min:5|max:1000',
]);
$rules = ConfigCacheService::get('app.rules');
$val = $request->input('rule');
if (! $rules) {
ConfigCacheService::put('app.rules', json_encode([$val]));
} else {
$json = json_decode($rules, true);
$count = count($json);
if ($count >= 30) {
return response()->json(['message' => 'Max rules limit reached, you can set up to 30 rules at a time.'], 400);
}
$json[] = $val;
ConfigCacheService::put('app.rules', json_encode(array_values($json)));
}
Cache::forget('api:v1:instance-data:rules');
Cache::forget('api:v1:instance-data-response-v1');
Cache::forget('api:v2:instance-data-response-v2');
Config::refresh();
return [$val];
}
public function settingsApiRulesDelete(Request $request)
{
$this->validate($request, [
'rule' => 'required|string',
]);
$rules = ConfigCacheService::get('app.rules');
$val = $request->input('rule');
if (! $rules) {
return [];
} else {
$json = json_decode($rules, true);
$idx = array_search($val, $json);
if ($idx !== false) {
unset($json[$idx]);
$json = array_values($json);
}
ConfigCacheService::put('app.rules', json_encode(array_values($json)));
}
Cache::forget('api:v1:instance-data:rules');
Cache::forget('api:v1:instance-data-response-v1');
Cache::forget('api:v2:instance-data-response-v2');
Config::refresh();
return response()->json($json);
}
public function settingsApiRulesDeleteAll(Request $request)
{
$rules = ConfigCacheService::get('app.rules');
if (! $rules) {
return [];
} else {
ConfigCacheService::put('app.rules', json_encode([]));
}
Cache::forget('api:v1:instance-data:rules');
Cache::forget('api:v1:instance-data-response-v1');
Cache::forget('api:v2:instance-data-response-v2');
Config::refresh();
return response()->json([]);
}
public function settingsApiAutofollowDelete(Request $request)
{
$this->validate($request, [
'username' => 'required|string',
]);
$username = $request->input('username');
$names = [];
$existing = config_cache('account.autofollow_usernames');
if ($existing) {
$names = explode(',', $existing);
}
if (in_array($username, $names)) {
$key = array_search($username, $names);
if ($key !== false) {
unset($names[$key]);
}
}
ConfigCacheService::put('account.autofollow_usernames', implode(',', $names));
return response()->json(['accounts' => array_values($names)]);
}
public function settingsApiAutofollowAdd(Request $request)
{
$this->validate($request, [
'username' => 'required|string',
]);
$username = $request->input('username');
$names = [];
$existing = config_cache('account.autofollow_usernames');
if ($existing) {
$names = explode(',', $existing);
}
if ($existing && count($names)) {
if (count($names) >= 5) {
return response()->json(['message' => 'You can only add up to 5 accounts to be autofollowed.'], 400);
}
if (in_array(strtolower($username), array_map('strtolower', $names))) {
return response()->json(['message' => 'User already exists, please try again.'], 400);
}
}
$p = User::whereUsername($username)->whereNull('status')->first();
if (! $p || in_array($p->username, $names)) {
abort(404);
}
array_push($names, $p->username);
ConfigCacheService::put('account.autofollow_usernames', implode(',', $names));
return response()->json(['accounts' => array_values($names)]);
}
public function settingsApiUpdateType(Request $request, $type)
{
abort_unless(in_array($type, [
'posts',
'platform',
'home',
'landing',
'branding',
'media',
'users',
'storage',
]), 400);
switch ($type) {
case 'home':
return $this->settingsApiUpdateHomeType($request);
break;
case 'landing':
return $this->settingsApiUpdateLandingType($request);
break;
case 'posts':
return $this->settingsApiUpdatePostsType($request);
break;
case 'platform':
return $this->settingsApiUpdatePlatformType($request);
break;
case 'branding':
return $this->settingsApiUpdateBrandingType($request);
break;
case 'media':
return $this->settingsApiUpdateMediaType($request);
break;
case 'users':
return $this->settingsApiUpdateUsersType($request);
break;
case 'storage':
return $this->settingsApiUpdateStorageType($request);
break;
default:
abort(404);
break;
}
}
public function settingsApiUpdateHomeType($request)
{
$this->validate($request, [
'registration_status' => 'required|in:open,filtered,closed',
'cloud_storage' => 'required',
'activitypub_enabled' => 'required',
'account_migration' => 'required',
'mobile_apis' => 'required',
'stories' => 'required',
'instagram_import' => 'required',
'autospam_enabled' => 'required',
]);
$regStatus = $request->input('registration_status');
ConfigCacheService::put('pixelfed.open_registration', $regStatus === 'open');
ConfigCacheService::put('instance.curated_registration.enabled', $regStatus === 'filtered');
$cloudStorage = $request->boolean('cloud_storage');
if ($cloudStorage !== (bool) config_cache('pixelfed.cloud_storage')) {
if (! $cloudStorage) {
ConfigCacheService::put('pixelfed.cloud_storage', false);
} else {
$cloud_disk = config('filesystems.cloud');
$cloud_ready = ! empty(config('filesystems.disks.'.$cloud_disk.'.key')) && ! empty(config('filesystems.disks.'.$cloud_disk.'.secret'));
if (! $cloud_ready) {
return redirect()->back()->withErrors(['cloud_storage' => 'Must configure cloud storage before enabling!']);
} else {
ConfigCacheService::put('pixelfed.cloud_storage', true);
}
}
}
ConfigCacheService::put('federation.activitypub.enabled', $request->boolean('activitypub_enabled'));
ConfigCacheService::put('federation.migration', $request->boolean('account_migration'));
ConfigCacheService::put('pixelfed.oauth_enabled', $request->boolean('mobile_apis'));
ConfigCacheService::put('instance.stories.enabled', $request->boolean('stories'));
ConfigCacheService::put('pixelfed.import.instagram.enabled', $request->boolean('instagram_import'));
ConfigCacheService::put('pixelfed.bouncer.enabled', $request->boolean('autospam_enabled'));
Cache::forget('api:v1:instance-data-response-v1');
Cache::forget('api:v2:instance-data-response-v2');
Cache::forget('api:v1:instance-data:contact');
Config::refresh();
return $request->all();
}
public function settingsApiUpdateLandingType($request)
{
$this->validate($request, [
'current_admin' => 'required',
'show_directory' => 'required',
'show_explore' => 'required',
]);
ConfigCacheService::put('instance.admin.pid', $request->input('current_admin'));
ConfigCacheService::put('instance.landing.show_directory', $request->boolean('show_directory'));
ConfigCacheService::put('instance.landing.show_explore', $request->boolean('show_explore'));
Cache::forget('api:v1:instance-data:rules');
Cache::forget('api:v1:instance-data-response-v1');
Cache::forget('api:v2:instance-data-response-v2');
Cache::forget('api:v1:instance-data:contact');
Config::refresh();
return $request->all();
}
public function settingsApiUpdateMediaType($request)
{
$this->validate($request, [
'image_quality' => 'required|integer|min:1|max:100',
'max_album_length' => 'required|integer|min:1|max:20',
'max_photo_size' => 'required|integer|min:100|max:50000',
'media_types' => 'required',
'optimize_image' => 'required',
'optimize_video' => 'required',
]);
$mediaTypes = $request->input('media_types');
$mediaArray = explode(',', $mediaTypes);
foreach ($mediaArray as $mediaType) {
if (! in_array($mediaType, ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'video/mp4'])) {
return redirect()->back()->withErrors(['media_types' => 'Invalid media type']);
}
}
ConfigCacheService::put('pixelfed.media_types', $request->input('media_types'));
ConfigCacheService::put('pixelfed.image_quality', $request->input('image_quality'));
ConfigCacheService::put('pixelfed.max_album_length', $request->input('max_album_length'));
ConfigCacheService::put('pixelfed.max_photo_size', $request->input('max_photo_size'));
ConfigCacheService::put('pixelfed.optimize_image', $request->boolean('optimize_image'));
ConfigCacheService::put('pixelfed.optimize_video', $request->boolean('optimize_video'));
Cache::forget('api:v1:instance-data:rules');
Cache::forget('api:v1:instance-data-response-v1');
Cache::forget('api:v2:instance-data-response-v2');
Cache::forget('api:v1:instance-data:contact');
Config::refresh();
return $request->all();
}
public function settingsApiUpdateBrandingType($request)
{
$this->validate($request, [
'name' => 'required',
'short_description' => 'required',
'long_description' => 'required',
]);
ConfigCacheService::put('app.name', $request->input('name'));
ConfigCacheService::put('app.short_description', $request->input('short_description'));
ConfigCacheService::put('app.description', $request->input('long_description'));
Cache::forget('api:v1:instance-data:rules');
Cache::forget('api:v1:instance-data-response-v1');
Cache::forget('api:v2:instance-data-response-v2');
Cache::forget('api:v1:instance-data:contact');
Config::refresh();
return $request->all();
}
public function settingsApiUpdatePostsType($request)
{
$this->validate($request, [
'max_caption_length' => 'required|integer|min:5|max:10000',
'max_altext_length' => 'required|integer|min:5|max:40000',
]);
ConfigCacheService::put('pixelfed.max_caption_length', $request->input('max_caption_length'));
ConfigCacheService::put('pixelfed.max_altext_length', $request->input('max_altext_length'));
$res = [
'max_caption_length' => $request->input('max_caption_length'),
'max_altext_length' => $request->input('max_altext_length'),
];
Cache::forget('api:v1:instance-data:rules');
Cache::forget('api:v1:instance-data-response-v1');
Cache::forget('api:v2:instance-data-response-v2');
Config::refresh();
return $res;
}
public function settingsApiUpdatePlatformType($request)
{
$this->validate($request, [
'allow_app_registration' => 'required',
'app_registration_rate_limit_attempts' => 'required|integer|min:1',
'app_registration_rate_limit_decay' => 'required|integer|min:1',
'app_registration_confirm_rate_limit_attempts' => 'required|integer|min:1',
'app_registration_confirm_rate_limit_decay' => 'required|integer|min:1',
'allow_post_embeds' => 'required',
'allow_profile_embeds' => 'required',
'captcha_enabled' => 'required',
'captcha_on_login' => 'required_if_accepted:captcha_enabled',
'captcha_on_register' => 'required_if_accepted:captcha_enabled',
'captcha_secret' => 'required_if_accepted:captcha_enabled',
'captcha_sitekey' => 'required_if_accepted:captcha_enabled',
'custom_emoji_enabled' => 'required',
]);
ConfigCacheService::put('pixelfed.allow_app_registration', $request->boolean('allow_app_registration'));
ConfigCacheService::put('pixelfed.app_registration_rate_limit_attempts', $request->input('app_registration_rate_limit_attempts'));
ConfigCacheService::put('pixelfed.app_registration_rate_limit_decay', $request->input('app_registration_rate_limit_decay'));
ConfigCacheService::put('pixelfed.app_registration_confirm_rate_limit_attempts', $request->input('app_registration_confirm_rate_limit_attempts'));
ConfigCacheService::put('pixelfed.app_registration_confirm_rate_limit_decay', $request->input('app_registration_confirm_rate_limit_decay'));
ConfigCacheService::put('instance.embed.post', $request->boolean('allow_post_embeds'));
ConfigCacheService::put('instance.embed.profile', $request->boolean('allow_profile_embeds'));
ConfigCacheService::put('federation.custom_emoji.enabled', $request->boolean('custom_emoji_enabled'));
$captcha = $request->boolean('captcha_enabled');
if ($captcha) {
$secret = $request->input('captcha_secret');
$sitekey = $request->input('captcha_sitekey');
if (config_cache('captcha.secret') != $secret && strpos($secret, '*') === false) {
ConfigCacheService::put('captcha.secret', $secret);
}
if (config_cache('captcha.sitekey') != $sitekey && strpos($sitekey, '*') === false) {
ConfigCacheService::put('captcha.sitekey', $sitekey);
}
ConfigCacheService::put('captcha.active.login', $request->boolean('captcha_on_login'));
ConfigCacheService::put('captcha.active.register', $request->boolean('captcha_on_register'));
ConfigCacheService::put('captcha.triggers.login.enabled', $request->boolean('captcha_on_login'));
ConfigCacheService::put('captcha.enabled', true);
} else {
ConfigCacheService::put('captcha.enabled', false);
}
$res = [
'allow_app_registration' => $request->boolean('allow_app_registration'),
'app_registration_rate_limit_attempts' => $request->input('app_registration_rate_limit_attempts'),
'app_registration_rate_limit_decay' => $request->input('app_registration_rate_limit_decay'),
'app_registration_confirm_rate_limit_attempts' => $request->input('app_registration_confirm_rate_limit_attempts'),
'app_registration_confirm_rate_limit_decay' => $request->input('app_registration_confirm_rate_limit_decay'),
'allow_post_embeds' => $request->boolean('allow_post_embeds'),
'allow_profile_embeds' => $request->boolean('allow_profile_embeds'),
'captcha_enabled' => $request->boolean('captcha_enabled'),
'captcha_on_login' => $request->boolean('captcha_on_login'),
'captcha_on_register' => $request->boolean('captcha_on_register'),
'captcha_secret' => $request->input('captcha_secret'),
'captcha_sitekey' => $request->input('captcha_sitekey'),
'custom_emoji_enabled' => $request->boolean('custom_emoji_enabled'),
];
Cache::forget('api:v1:instance-data:rules');
Cache::forget('api:v1:instance-data-response-v1');
Cache::forget('api:v2:instance-data-response-v2');
Config::refresh();
return $res;
}
public function settingsApiUpdateUsersType($request)
{
$this->validate($request, [
'require_email_verification' => 'required',
'enforce_account_limit' => 'required',
'max_account_size' => 'required|integer|min:50000',
'admin_autofollow' => 'required',
'admin_autofollow_accounts' => 'sometimes',
'max_user_blocks' => 'required|integer|min:0|max:5000',
'max_user_mutes' => 'required|integer|min:0|max:5000',
'max_domain_blocks' => 'required|integer|min:0|max:5000',
]);
$adminAutofollow = $request->boolean('admin_autofollow');
$adminAutofollowAccounts = $request->input('admin_autofollow_accounts');
if ($adminAutofollow) {
if ($request->filled('admin_autofollow_accounts')) {
$names = [];
$existing = config_cache('account.autofollow_usernames');
if ($existing) {
$names = explode(',', $existing);
foreach (array_map('strtolower', $adminAutofollowAccounts) as $afc) {
if (in_array(strtolower($afc), array_map('strtolower', $names))) {
continue;
}
$names[] = $afc;
}
} else {
$names = $adminAutofollowAccounts;
}
if (! $names || count($names) == 0) {
return response()->json(['message' => 'You need to assign autofollow accounts before you can enable it.'], 400);
}
if (count($names) > 5) {
return response()->json(['message' => 'You can only add up to 5 accounts to be autofollowed.'.json_encode($names)], 400);
}
$autofollows = User::whereIn('username', $names)->whereNull('status')->pluck('username');
$adminAutofollowAccounts = $autofollows->implode(',');
ConfigCacheService::put('account.autofollow_usernames', $adminAutofollowAccounts);
} else {
return response()->json(['message' => 'You need to assign autofollow accounts before you can enable it.'], 400);
}
}
ConfigCacheService::put('pixelfed.enforce_email_verification', $request->boolean('require_email_verification'));
ConfigCacheService::put('pixelfed.enforce_account_limit', $request->boolean('enforce_account_limit'));
ConfigCacheService::put('pixelfed.max_account_size', $request->input('max_account_size'));
ConfigCacheService::put('account.autofollow', $request->boolean('admin_autofollow'));
ConfigCacheService::put('instance.user_filters.max_user_blocks', (int) $request->input('max_user_blocks'));
ConfigCacheService::put('instance.user_filters.max_user_mutes', (int) $request->input('max_user_mutes'));
ConfigCacheService::put('instance.user_filters.max_domain_blocks', (int) $request->input('max_domain_blocks'));
$res = [
'require_email_verification' => $request->boolean('require_email_verification'),
'enforce_account_limit' => $request->boolean('enforce_account_limit'),
'admin_autofollow' => $request->boolean('admin_autofollow'),
'admin_autofollow_accounts' => $adminAutofollowAccounts,
'max_user_blocks' => $request->input('max_user_blocks'),
'max_user_mutes' => $request->input('max_user_mutes'),
'max_domain_blocks' => $request->input('max_domain_blocks'),
];
Cache::forget('api:v1:instance-data:rules');
Cache::forget('api:v1:instance-data-response-v1');
Cache::forget('api:v2:instance-data-response-v2');
Config::refresh();
return $res;
}
public function settingsApiUpdateStorageType($request)
{
$this->validate($request, [
'primary_disk' => 'required|in:local,cloud',
'update_disk' => 'sometimes',
'disk_config' => 'required_if_accepted:update_disk',
'disk_config.driver' => 'required|in:s3,spaces',
'disk_config.key' => 'required',
'disk_config.secret' => 'required',
'disk_config.region' => 'required',
'disk_config.bucket' => 'required',
'disk_config.visibility' => 'required',
'disk_config.endpoint' => 'required',
'disk_config.url' => 'nullable',
]);
ConfigCacheService::put('pixelfed.cloud_storage', $request->input('primary_disk') === 'cloud');
$res = [
'primary_disk' => $request->input('primary_disk'),
];
if ($request->has('update_disk')) {
$res['disk_config'] = $request->input('disk_config');
$changes = [];
$dkey = $request->input('disk_config.driver') === 's3' ? 'filesystems.disks.s3.' : 'filesystems.disks.spaces.';
$key = $request->input('disk_config.key');
$ckey = null;
$secret = $request->input('disk_config.secret');
$csecret = null;
$region = $request->input('disk_config.region');
$bucket = $request->input('disk_config.bucket');
$visibility = $request->input('disk_config.visibility');
$url = $request->input('disk_config.url');
$endpoint = $request->input('disk_config.endpoint');
if (strpos($key, '*') === false && $key != config_cache($dkey.'key')) {
array_push($changes, 'key');
} else {
$ckey = config_cache($dkey.'key');
}
if (strpos($secret, '*') === false && $secret != config_cache($dkey.'secret')) {
array_push($changes, 'secret');
} else {
$csecret = config_cache($dkey.'secret');
}
if ($region != config_cache($dkey.'region')) {
array_push($changes, 'region');
}
if ($bucket != config_cache($dkey.'bucket')) {
array_push($changes, 'bucket');
}
if ($visibility != config_cache($dkey.'visibility')) {
array_push($changes, 'visibility');
}
if ($url != config_cache($dkey.'url')) {
array_push($changes, 'url');
}
if ($endpoint != config_cache($dkey.'endpoint')) {
array_push($changes, 'endpoint');
}
if ($changes && count($changes)) {
$isValid = FilesystemService::getVerifyCredentials(
$ckey ?? $key,
$csecret ?? $secret,
$region,
$bucket,
$endpoint,
);
if (! $isValid) {
return response()->json(['error' => true, 's3_vce' => true, 'message' => "<div class='border border-danger text-danger p-3 font-weight-bold rounded-lg'>The S3/Spaces credentials you provided are invalid, or the bucket does not have the proper permissions.</div><br/>Please check all fields and try again.<br/><br/><strong>Any cloud storage configuration changes you made have NOT been saved due to invalid credentials.</strong>"], 400);
}
}
$res['changes'] = json_encode($changes);
}
Cache::forget('api:v1:instance-data:rules');
Cache::forget('api:v1:instance-data-response-v1');
Cache::forget('api:v2:instance-data-response-v2');
Config::refresh();
return $res;
}
}

View File

@ -424,7 +424,7 @@ class AdminController extends Controller
public function customEmojiHome(Request $request)
{
if(!(bool) config_cache('federation.custom_emoji.enabled')) {
if(!config('federation.custom_emoji.enabled')) {
return view('admin.custom-emoji.not-enabled');
}
$this->validate($request, [
@ -497,7 +497,7 @@ class AdminController extends Controller
public function customEmojiToggleActive(Request $request, $id)
{
abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
abort_unless(config('federation.custom_emoji.enabled'), 404);
$emoji = CustomEmoji::findOrFail($id);
$emoji->disabled = !$emoji->disabled;
$emoji->save();
@ -508,13 +508,13 @@ class AdminController extends Controller
public function customEmojiAdd(Request $request)
{
abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
abort_unless(config('federation.custom_emoji.enabled'), 404);
return view('admin.custom-emoji.add');
}
public function customEmojiStore(Request $request)
{
abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
abort_unless(config('federation.custom_emoji.enabled'), 404);
$this->validate($request, [
'shortcode' => [
'required',
@ -545,7 +545,7 @@ class AdminController extends Controller
public function customEmojiDelete(Request $request, $id)
{
abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
abort_unless(config('federation.custom_emoji.enabled'), 404);
$emoji = CustomEmoji::findOrFail($id);
Storage::delete("public/{$emoji->media_path}");
Cache::forget('pf:custom_emoji');
@ -555,7 +555,7 @@ class AdminController extends Controller
public function customEmojiShowDuplicates(Request $request, $id)
{
abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
abort_unless(config('federation.custom_emoji.enabled'), 404);
$emoji = CustomEmoji::orderBy('id')->whereDisabled(false)->whereShortcode($id)->firstOrFail();
$emojis = CustomEmoji::whereShortcode($id)->where('id', '!=', $emoji->id)->cursorPaginate(10);
return view('admin.custom-emoji.duplicates', compact('emoji', 'emojis'));

View File

@ -1,340 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Mail\CuratedRegisterAcceptUser;
use App\Mail\CuratedRegisterRejectUser;
use App\Mail\CuratedRegisterRequestDetailsFromUser;
use App\Models\CuratedRegister;
use App\Models\CuratedRegisterActivity;
use App\Models\CuratedRegisterTemplate;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
class AdminCuratedRegisterController extends Controller
{
public function __construct()
{
$this->middleware(['auth', 'admin']);
}
public function index(Request $request)
{
$this->validate($request, [
'filter' => 'sometimes|in:open,all,awaiting,approved,rejected,responses',
'sort' => 'sometimes|in:asc,desc',
]);
$filter = $request->input('filter', 'open');
$sort = $request->input('sort', 'asc');
$records = CuratedRegister::when($filter, function ($q, $filter) {
if ($filter === 'open') {
return $q->where('is_rejected', false)
->where(function ($query) {
return $query->where('user_has_responded', true)->orWhere('is_awaiting_more_info', false);
})
->whereNotNull('email_verified_at')
->whereIsClosed(false);
} elseif ($filter === 'all') {
return $q;
} elseif ($filter === 'responses') {
return $q->whereIsClosed(false)
->whereNotNull('email_verified_at')
->where('user_has_responded', true)
->where('is_awaiting_more_info', true);
} elseif ($filter === 'awaiting') {
return $q->whereIsClosed(false)
->where('is_rejected', false)
->where('is_approved', false)
->where('user_has_responded', false)
->where('is_awaiting_more_info', true);
} elseif ($filter === 'approved') {
return $q->whereIsClosed(true)->whereIsApproved(true);
} elseif ($filter === 'rejected') {
return $q->whereIsClosed(true)->whereIsRejected(true);
}
})
->when($sort, function ($query, $sort) {
return $query->orderBy('id', $sort);
})
->paginate(10)
->withQueryString();
return view('admin.curated-register.index', compact('records', 'filter'));
}
public function show(Request $request, $id)
{
$record = CuratedRegister::findOrFail($id);
return view('admin.curated-register.show', compact('record'));
}
public function apiActivityLog(Request $request, $id)
{
$record = CuratedRegister::findOrFail($id);
$res = collect([
[
'id' => 1,
'action' => 'created',
'title' => 'Onboarding application created',
'message' => null,
'link' => null,
'timestamp' => $record->created_at,
],
]);
if ($record->email_verified_at) {
$res->push([
'id' => 3,
'action' => 'email_verified_at',
'title' => 'Applicant successfully verified email address',
'message' => null,
'link' => null,
'timestamp' => $record->email_verified_at,
]);
}
$activities = CuratedRegisterActivity::whereRegisterId($record->id)->get();
$idx = 4;
$userResponses = collect([]);
foreach ($activities as $activity) {
$idx++;
if ($activity->type === 'user_resend_email_confirmation') {
continue;
}
if ($activity->from_user) {
$userResponses->push($activity);
continue;
}
$res->push([
'id' => $idx,
'aid' => $activity->id,
'action' => $activity->type,
'title' => $activity->from_admin ? 'Admin requested info' : 'User responded',
'message' => $activity->message,
'link' => $activity->adminReviewUrl(),
'timestamp' => $activity->created_at,
]);
}
foreach ($userResponses as $ur) {
$res = $res->map(function ($r) use ($ur) {
if (! isset($r['aid'])) {
return $r;
}
if ($ur->reply_to_id === $r['aid']) {
$r['user_response'] = $ur;
return $r;
}
return $r;
});
}
if ($record->is_approved) {
$idx++;
$res->push([
'id' => $idx,
'action' => 'approved',
'title' => 'Application Approved',
'message' => null,
'link' => null,
'timestamp' => $record->action_taken_at,
]);
} elseif ($record->is_rejected) {
$idx++;
$res->push([
'id' => $idx,
'action' => 'rejected',
'title' => 'Application Rejected',
'message' => null,
'link' => null,
'timestamp' => $record->action_taken_at,
]);
}
return $res->reverse()->values();
}
public function apiMessagePreviewStore(Request $request, $id)
{
$record = CuratedRegister::findOrFail($id);
return $request->all();
}
public function apiMessageSendStore(Request $request, $id)
{
$this->validate($request, [
'message' => 'required|string|min:5|max:3000',
]);
$record = CuratedRegister::findOrFail($id);
abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email');
$activity = new CuratedRegisterActivity;
$activity->register_id = $record->id;
$activity->admin_id = $request->user()->id;
$activity->secret_code = Str::random(32);
$activity->type = 'request_details';
$activity->from_admin = true;
$activity->message = $request->input('message');
$activity->save();
$record->is_awaiting_more_info = true;
$record->user_has_responded = false;
$record->save();
Mail::to($record->email)->send(new CuratedRegisterRequestDetailsFromUser($record, $activity));
return $request->all();
}
public function previewDetailsMessageShow(Request $request, $id)
{
$record = CuratedRegister::findOrFail($id);
abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email');
$activity = new CuratedRegisterActivity;
$activity->message = $request->input('message');
return new \App\Mail\CuratedRegisterRequestDetailsFromUser($record, $activity);
}
public function previewMessageShow(Request $request, $id)
{
$record = CuratedRegister::findOrFail($id);
abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email');
$record->message = $request->input('message');
return new \App\Mail\CuratedRegisterSendMessage($record);
}
public function apiHandleReject(Request $request, $id)
{
$this->validate($request, [
'action' => 'required|in:reject-email,reject-silent',
]);
$action = $request->input('action');
$record = CuratedRegister::findOrFail($id);
abort_if($record->email_verified_at === null, 400, 'Cannot reject an unverified email');
$record->is_rejected = true;
$record->is_closed = true;
$record->action_taken_at = now();
$record->save();
if ($action === 'reject-email') {
Mail::to($record->email)->send(new CuratedRegisterRejectUser($record));
}
return [200];
}
public function apiHandleApprove(Request $request, $id)
{
$record = CuratedRegister::findOrFail($id);
abort_if($record->email_verified_at === null, 400, 'Cannot reject an unverified email');
$record->is_approved = true;
$record->is_closed = true;
$record->action_taken_at = now();
$record->save();
if (User::withTrashed()->whereEmail($record->email)->exists()) {
return [200];
}
$user = User::create([
'name' => $record->username,
'username' => $record->username,
'email' => $record->email,
'password' => $record->password,
'app_register_ip' => $record->ip_address,
'email_verified_at' => now(),
'register_source' => 'cur_onboarding',
]);
Mail::to($record->email)->send(new CuratedRegisterAcceptUser($record));
return [200];
}
public function templates(Request $request)
{
$templates = CuratedRegisterTemplate::paginate(10);
return view('admin.curated-register.templates', compact('templates'));
}
public function templateCreate(Request $request)
{
return view('admin.curated-register.template-create');
}
public function templateEdit(Request $request, $id)
{
$template = CuratedRegisterTemplate::findOrFail($id);
return view('admin.curated-register.template-edit', compact('template'));
}
public function templateEditStore(Request $request, $id)
{
$this->validate($request, [
'name' => 'required|string|max:30',
'content' => 'required|string|min:5|max:3000',
'description' => 'nullable|sometimes|string|max:1000',
'active' => 'sometimes',
]);
$template = CuratedRegisterTemplate::findOrFail($id);
$template->name = $request->input('name');
$template->content = $request->input('content');
$template->description = $request->input('description');
$template->is_active = $request->boolean('active');
$template->save();
return redirect()->back()->with('status', 'Successfully updated template!');
}
public function templateDelete(Request $request, $id)
{
$template = CuratedRegisterTemplate::findOrFail($id);
$template->delete();
return redirect(route('admin.curated-onboarding.templates'))->with('status', 'Successfully deleted template!');
}
public function templateStore(Request $request)
{
$this->validate($request, [
'name' => 'required|string|max:30',
'content' => 'required|string|min:5|max:3000',
'description' => 'nullable|sometimes|string|max:1000',
'active' => 'sometimes',
]);
CuratedRegisterTemplate::create([
'name' => $request->input('name'),
'content' => $request->input('content'),
'description' => $request->input('description'),
'is_active' => $request->boolean('active'),
]);
return redirect(route('admin.curated-onboarding.templates'))->with('status', 'Successfully created new template!');
}
public function getActiveTemplates(Request $request)
{
$templates = CuratedRegisterTemplate::whereIsActive(true)
->orderBy('order')
->get()
->map(function ($tmp) {
return [
'name' => $tmp->name,
'content' => $tmp->content,
];
});
return response()->json($templates);
}
}

View File

@ -1,123 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\AdminShadowFilter;
use App\Profile;
use App\Services\AccountService;
use App\Services\AdminShadowFilterService;
class AdminShadowFilterController extends Controller
{
public function __construct()
{
$this->middleware(['auth','admin']);
}
public function home(Request $request)
{
$filter = $request->input('filter');
$searchQuery = $request->input('q');
$filters = AdminShadowFilter::whereHas('profile')
->when($filter, function($q, $filter) {
if($filter == 'all') {
return $q;
} else if($filter == 'inactive') {
return $q->whereActive(false);
} else {
return $q;
}
}, function($q, $filter) {
return $q->whereActive(true);
})
->when($searchQuery, function($q, $searchQuery) {
$ids = Profile::where('username', 'like', '%' . $searchQuery . '%')
->limit(100)
->pluck('id')
->toArray();
return $q->where('item_type', 'App\Profile')->whereIn('item_id', $ids);
})
->latest()
->paginate(10)
->withQueryString();
return view('admin.asf.home', compact('filters'));
}
public function create(Request $request)
{
return view('admin.asf.create');
}
public function edit(Request $request, $id)
{
$filter = AdminShadowFilter::findOrFail($id);
$profile = AccountService::get($filter->item_id);
return view('admin.asf.edit', compact('filter', 'profile'));
}
public function store(Request $request)
{
$this->validate($request, [
'username' => 'required',
'active' => 'sometimes',
'note' => 'sometimes',
'hide_from_public_feeds' => 'sometimes'
]);
$profile = Profile::whereUsername($request->input('username'))->first();
if(!$profile) {
return back()->withErrors(['Invalid account']);
}
if($profile->user && $profile->user->is_admin) {
return back()->withErrors(['Cannot filter an admin account']);
}
$active = $request->has('active') && $request->has('hide_from_public_feeds');
AdminShadowFilter::updateOrCreate([
'item_id' => $profile->id,
'item_type' => get_class($profile)
], [
'is_local' => $profile->domain === null,
'note' => $request->input('note'),
'hide_from_public_feeds' => $request->has('hide_from_public_feeds'),
'admin_id' => $request->user()->profile_id,
'active' => $active
]);
AdminShadowFilterService::refresh();
return redirect('/i/admin/asf/home');
}
public function storeEdit(Request $request, $id)
{
$this->validate($request, [
'active' => 'sometimes',
'note' => 'sometimes',
'hide_from_public_feeds' => 'sometimes'
]);
$filter = AdminShadowFilter::findOrFail($id);
$profile = Profile::findOrFail($filter->item_id);
if($profile->user && $profile->user->is_admin) {
return back()->withErrors(['Cannot filter an admin account']);
}
$active = $request->has('active');
$filter->active = $active;
$filter->hide_from_public_feeds = $request->has('hide_from_public_feeds');
$filter->note = $request->input('note');
$filter->save();
AdminShadowFilterService::refresh();
return redirect('/i/admin/asf/home');
}
}

View File

@ -40,20 +40,16 @@ class AdminApiController extends Controller
{
public function supported(Request $request)
{
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_if(!$request->user(), 404);
abort_unless($request->user()->is_admin == 1, 404);
abort_unless($request->user()->tokenCan('admin:read'), 404);
return response()->json(['supported' => true]);
}
public function getStats(Request $request)
{
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_if(!$request->user(), 404);
abort_unless($request->user()->is_admin == 1, 404);
abort_unless($request->user()->tokenCan('admin:read'), 404);
$res = AdminStatsService::summary();
$res['autospam_count'] = AccountInterstitial::whereType('post.autospam')
@ -64,10 +60,8 @@ class AdminApiController extends Controller
public function autospam(Request $request)
{
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_if(!$request->user(), 404);
abort_unless($request->user()->is_admin == 1, 404);
abort_unless($request->user()->tokenCan('admin:read'), 404);
$appeals = AccountInterstitial::whereType('post.autospam')
->whereNull('appeal_handled_at')
@ -101,10 +95,8 @@ class AdminApiController extends Controller
public function autospamHandle(Request $request)
{
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_if(!$request->user(), 404);
abort_unless($request->user()->is_admin == 1, 404);
abort_unless($request->user()->tokenCan('admin:write'), 404);
$this->validate($request, [
'action' => 'required|in:dismiss,approve,dismiss-all,approve-all,delete-post,delete-account',
@ -247,10 +239,8 @@ class AdminApiController extends Controller
public function modReports(Request $request)
{
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_if(!$request->user(), 404);
abort_unless($request->user()->is_admin == 1, 404);
abort_unless($request->user()->tokenCan('admin:read'), 404);
$reports = Report::whereNull('admin_seen')
->orderBy('created_at','desc')
@ -295,10 +285,8 @@ class AdminApiController extends Controller
public function modReportHandle(Request $request)
{
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_if(!$request->user(), 404);
abort_unless($request->user()->is_admin == 1, 404);
abort_unless($request->user()->tokenCan('admin:write'), 404);
$this->validate($request, [
'action' => 'required|string',
@ -355,11 +343,8 @@ class AdminApiController extends Controller
public function getConfiguration(Request $request)
{
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_if(!$request->user(), 404);
abort_unless($request->user()->is_admin == 1, 404);
abort_unless($request->user()->tokenCan('admin:read'), 404);
abort_unless(config('instance.enable_cc'), 400);
return collect([
@ -401,11 +386,8 @@ class AdminApiController extends Controller
public function updateConfiguration(Request $request)
{
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_if(!$request->user(), 404);
abort_unless($request->user()->is_admin == 1, 404);
abort_unless($request->user()->tokenCan('admin:write'), 404);
abort_unless(config('instance.enable_cc'), 400);
$this->validate($request, [
@ -466,11 +448,8 @@ class AdminApiController extends Controller
public function getUsers(Request $request)
{
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_if(!$request->user(), 404);
abort_unless($request->user()->is_admin == 1, 404);
abort_unless($request->user()->tokenCan('admin:read'), 404);
$this->validate($request, [
'sort' => 'sometimes|in:asc,desc',
]);
@ -487,10 +466,8 @@ class AdminApiController extends Controller
public function getUser(Request $request)
{
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_if(!$request->user(), 404);
abort_unless($request->user()->is_admin == 1, 404);
abort_unless($request->user()->tokenCan('admin:read'), 404);
$id = $request->input('user_id');
$key = 'pf-admin-api:getUser:byId:' . $id;
@ -520,10 +497,8 @@ class AdminApiController extends Controller
public function userAdminAction(Request $request)
{
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_if(!$request->user(), 404);
abort_unless($request->user()->is_admin == 1, 404);
abort_unless($request->user()->tokenCan('admin:write'), 404);
$this->validate($request, [
'id' => 'required',
@ -694,10 +669,8 @@ class AdminApiController extends Controller
public function instances(Request $request)
{
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_if(!$request->user(), 404);
abort_unless($request->user()->is_admin == 1, 404);
abort_unless($request->user()->tokenCan('admin:write'), 404);
$this->validate($request, [
'q' => 'sometimes',
@ -734,10 +707,8 @@ class AdminApiController extends Controller
public function getInstance(Request $request)
{
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_if(!$request->user(), 404);
abort_unless($request->user()->is_admin == 1, 404);
abort_unless($request->user()->tokenCan('admin:read'), 404);
$id = $request->input('id');
$res = Instance::findOrFail($id);
@ -747,10 +718,8 @@ class AdminApiController extends Controller
public function moderateInstance(Request $request)
{
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_if(!$request->user(), 404);
abort_unless($request->user()->is_admin == 1, 404);
abort_unless($request->user()->tokenCan('admin:write'), 404);
$this->validate($request, [
'id' => 'required',
@ -773,10 +742,8 @@ class AdminApiController extends Controller
public function refreshInstanceStats(Request $request)
{
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_if(!$request->user(), 404);
abort_unless($request->user()->is_admin == 1, 404);
abort_unless($request->user()->tokenCan('admin:write'), 404);
$this->validate($request, [
'id' => 'required',
@ -793,10 +760,8 @@ class AdminApiController extends Controller
public function getAllStats(Request $request)
{
abort_if(!$request->user() || !$request->user()->token(), 404);
abort_if(!$request->user(), 404);
abort_unless($request->user()->is_admin === 1, 404);
abort_unless($request->user()->tokenCan('admin:read'), 404);
if($request->has('refresh')) {
Cache::forget('admin-api:instance-all-stats-v1');

File diff suppressed because it is too large Load Diff

View File

@ -2,41 +2,40 @@
namespace App\Http\Controllers\Api;
use Cache;
use DB;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use App\AccountLog;
use App\EmailVerification;
use App\Http\Controllers\Controller;
use App\Http\Resources\StatusStateless;
use App\Jobs\ReportPipeline\ReportNotifyAdminViaEmail;
use App\Jobs\StatusPipeline\RemoteStatusDelete;
use App\Jobs\StatusPipeline\StatusDelete;
use App\Mail\ConfirmAppEmail;
use App\Mail\PasswordChange;
use App\Place;
use App\Profile;
use App\Report;
use App\Services\AccountService;
use App\Services\BouncerService;
use App\Services\EmailService;
use App\Services\FollowerService;
use App\Services\NetworkTimelineService;
use App\Services\ProfileStatusService;
use App\Services\PublicTimelineService;
use App\Services\StatusService;
use App\Status;
use App\Report;
use App\Profile;
use App\StatusArchived;
use App\User;
use App\UserSetting;
use App\Services\AccountService;
use App\Services\StatusService;
use App\Services\ProfileStatusService;
use App\Services\PublicTimelineService;
use App\Services\NetworkTimelineService;
use App\Util\Lexer\RestrictedNames;
use Cache;
use DB;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\RateLimiter;
use App\Services\BouncerService;
use App\Services\EmailService;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Hash;
use Jenssegers\Agent\Agent;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
use Mail;
use App\Mail\PasswordChange;
use App\Mail\ConfirmAppEmail;
use App\Http\Resources\StatusStateless;
use App\Jobs\StatusPipeline\StatusDelete;
use App\Jobs\ReportPipeline\ReportNotifyAdminViaEmail;
use Illuminate\Support\Facades\RateLimiter;
class ApiV1Dot1Controller extends Controller
{
@ -56,19 +55,17 @@ class ApiV1Dot1Controller extends Controller
public function error($msg, $code = 400, $extra = [], $headers = [])
{
$res = [
'msg' => $msg,
'code' => $code,
"msg" => $msg,
"code" => $code
];
return response()->json(array_merge($res, $extra), $code, $headers, JSON_UNESCAPED_SLASHES);
}
public function report(Request $request)
{
abort_if(! $request->user() || ! $request->user()->token(), 403);
abort_unless($request->user()->tokenCan('write'), 403);
$user = $request->user();
abort_if(!$user, 403);
abort_if($user->status != null, 403);
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
@ -88,19 +85,19 @@ class ApiV1Dot1Controller extends Controller
'copyright',
'impersonation',
'scam',
'terrorism',
'terrorism'
];
if (!$report_type || !$object_id || !$object_type) {
return $this->error('Invalid or missing parameters', 400, ['error_code' => 'ERROR_INVALID_PARAMS']);
return $this->error("Invalid or missing parameters", 400, ["error_code" => "ERROR_INVALID_PARAMS"]);
}
if (!in_array($report_type, $types)) {
return $this->error('Invalid report type', 400, ['error_code' => 'ERROR_TYPE_INVALID']);
return $this->error("Invalid report type", 400, ["error_code" => "ERROR_TYPE_INVALID"]);
}
if ($object_type === 'user' && $object_id == $user->profile_id) {
return $this->error('Cannot self report', 400, ['error_code' => 'ERROR_NO_SELF_REPORTS']);
if ($object_type === "user" && $object_id == $user->profile_id) {
return $this->error("Cannot self report", 400, ["error_code" => "ERROR_NO_SELF_REPORTS"]);
}
$rpid = null;
@ -109,7 +106,7 @@ class ApiV1Dot1Controller extends Controller
case 'post':
$object = Status::find($object_id);
if (!$object) {
return $this->error('Invalid object id', 400, ['error_code' => 'ERROR_INVALID_OBJECT_ID']);
return $this->error("Invalid object id", 400, ["error_code" => "ERROR_INVALID_OBJECT_ID"]);
}
$object_type = 'App\Status';
$exists = Report::whereUserId($user->id)
@ -123,7 +120,7 @@ class ApiV1Dot1Controller extends Controller
case 'user':
$object = Profile::find($object_id);
if (!$object) {
return $this->error('Invalid object id', 400, ['error_code' => 'ERROR_INVALID_OBJECT_ID']);
return $this->error("Invalid object id", 400, ["error_code" => "ERROR_INVALID_OBJECT_ID"]);
}
$object_type = 'App\Profile';
$exists = Report::whereUserId($user->id)
@ -134,16 +131,16 @@ class ApiV1Dot1Controller extends Controller
break;
default:
return $this->error('Invalid report type', 400, ['error_code' => 'ERROR_REPORT_OBJECT_TYPE_INVALID']);
return $this->error("Invalid report type", 400, ["error_code" => "ERROR_REPORT_OBJECT_TYPE_INVALID"]);
break;
}
if ($exists !== 0) {
return $this->error('Duplicate report', 400, ['error_code' => 'ERROR_REPORT_DUPLICATE']);
return $this->error("Duplicate report", 400, ["error_code" => "ERROR_REPORT_DUPLICATE"]);
}
if ($object->profile_id == $user->profile_id) {
return $this->error('Cannot self report', 400, ['error_code' => 'ERROR_NO_SELF_REPORTS']);
return $this->error("Cannot self report", 400, ["error_code" => "ERROR_NO_SELF_REPORTS"]);
}
$report = new Report;
@ -160,10 +157,9 @@ class ApiV1Dot1Controller extends Controller
}
$res = [
'msg' => 'Successfully sent report',
'code' => 200,
"msg" => "Successfully sent report",
"code" => 200
];
return $this->json($res);
}
@ -174,10 +170,9 @@ class ApiV1Dot1Controller extends Controller
*/
public function deleteAvatar(Request $request)
{
abort_if(! $request->user() || ! $request->user()->token(), 403);
abort_unless($request->user()->tokenCan('write'), 403);
$user = $request->user();
abort_if(!$user, 403);
abort_if($user->status != null, 403);
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
@ -215,10 +210,9 @@ class ApiV1Dot1Controller extends Controller
*/
public function accountPosts(Request $request, $id)
{
abort_if(! $request->user() || ! $request->user()->token(), 403);
abort_unless($request->user()->tokenCan('read'), 403);
$user = $request->user();
abort_if(!$user, 403);
abort_if($user->status != null, 403);
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
@ -256,10 +250,8 @@ class ApiV1Dot1Controller extends Controller
*/
public function accountChangePassword(Request $request)
{
abort_if(! $request->user() || ! $request->user()->token(), 403);
abort_unless($request->user()->tokenCan('write'), 403);
$user = $request->user();
abort_if(!$user, 403);
abort_if($user->status != null, 403);
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
abort_if(BouncerService::checkIp($request->ip()), 404);
@ -268,9 +260,9 @@ class ApiV1Dot1Controller extends Controller
$this->validate($request, [
'current_password' => 'bail|required|current_password',
'new_password' => 'required|min:' . config('pixelfed.min_password_length', 8),
'confirm_password' => 'required|same:new_password',
'confirm_password' => 'required|same:new_password'
],[
'current_password' => 'The password you entered is incorrect',
'current_password' => 'The password you entered is incorrect'
]);
$user->password = bcrypt($request->input('new_password'));
@ -299,10 +291,8 @@ class ApiV1Dot1Controller extends Controller
*/
public function accountLoginActivity(Request $request)
{
abort_if(! $request->user() || ! $request->user()->token(), 403);
abort_unless($request->user()->tokenCan('read'), 403);
$user = $request->user();
abort_if(!$user, 403);
abort_if($user->status != null, 403);
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
abort_if(BouncerService::checkIp($request->ip()), 404);
@ -318,7 +308,6 @@ class ApiV1Dot1Controller extends Controller
->get()
->map(function($item) use($agent, $currentIp) {
$agent->setUserAgent($item->user_agent);
return [
'id' => $item->id,
'action' => $item->action,
@ -328,7 +317,7 @@ class ApiV1Dot1Controller extends Controller
'device' => $agent->device(),
'browser' => $agent->browser(),
'platform' => $agent->platform(),
'created_at' => $item->created_at->format('c'),
'created_at' => $item->created_at->format('c')
];
});
@ -342,10 +331,8 @@ class ApiV1Dot1Controller extends Controller
*/
public function accountTwoFactor(Request $request)
{
abort_if(! $request->user() || ! $request->user()->token(), 403);
abort_unless($request->user()->tokenCan('read'), 403);
$user = $request->user();
abort_if(!$user, 403);
abort_if($user->status != null, 403);
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
@ -354,9 +341,8 @@ class ApiV1Dot1Controller extends Controller
$res = [
'active' => (bool) $user->{'2fa_enabled'},
'setup_at' => $user->{'2fa_setup_at'},
'setup_at' => $user->{'2fa_setup_at'}
];
return $this->json($res);
}
@ -367,10 +353,8 @@ class ApiV1Dot1Controller extends Controller
*/
public function accountEmailsFromPixelfed(Request $request)
{
abort_if(! $request->user() || ! $request->user()->token(), 403);
abort_unless($request->user()->tokenCan('read'), 403);
$user = $request->user();
abort_if(!$user, 403);
abort_if($user->status != null, 403);
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
abort_if(BouncerService::checkIp($request->ip()), 404);
@ -388,7 +372,7 @@ class ApiV1Dot1Controller extends Controller
'subject' => 'Confirm Email',
'to_address' => $user->email,
'from_address' => $from,
'created_at' => str_replace('@', 'at', $mail->created_at->format('M j, Y @ g:i:s A')),
'created_at' => str_replace('@', 'at', $mail->created_at->format('M j, Y @ g:i:s A'))
];
})
->toArray();
@ -405,7 +389,7 @@ class ApiV1Dot1Controller extends Controller
'subject' => 'Reset Password Notification',
'to_address' => $user->email,
'from_address' => $from,
'created_at' => str_replace('@', 'at', now()->parse($mail->created_at)->format('M j, Y @ g:i:s A')),
'created_at' => str_replace('@', 'at', now()->parse($mail->created_at)->format('M j, Y @ g:i:s A'))
];
})
->toArray();
@ -422,7 +406,7 @@ class ApiV1Dot1Controller extends Controller
'subject' => 'Password Change',
'to_address' => $user->email,
'from_address' => $from,
'created_at' => str_replace('@', 'at', now()->parse($mail->created_at)->format('M j, Y @ g:i:s A')),
'created_at' => str_replace('@', 'at', now()->parse($mail->created_at)->format('M j, Y @ g:i:s A'))
];
})
->toArray();
@ -444,10 +428,8 @@ class ApiV1Dot1Controller extends Controller
*/
public function accountApps(Request $request)
{
abort_if(! $request->user() || ! $request->user()->token(), 403);
abort_unless($request->user()->tokenCan('read'), 403);
$user = $request->user();
abort_if(!$user, 403);
abort_if($user->status != null, 403);
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
@ -462,7 +444,7 @@ class ApiV1Dot1Controller extends Controller
'scopes' => $token->scopes,
'revoked' => $token->revoked,
'created_at' => str_replace('@', 'at', now()->parse($token->created_at)->format('M j, Y @ g:i:s A')),
'expires_at' => str_replace('@', 'at', now()->parse($token->expires_at)->format('M j, Y @ g:i:s A')),
'expires_at' => str_replace('@', 'at', now()->parse($token->expires_at)->format('M j, Y @ g:i:s A'))
];
});
@ -473,22 +455,21 @@ class ApiV1Dot1Controller extends Controller
{
return [
'open' => (bool) config_cache('pixelfed.open_registration'),
'iara' => (bool) config_cache('pixelfed.allow_app_registration'),
'iara' => config('pixelfed.allow_app_registration')
];
}
public function inAppRegistration(Request $request)
{
abort_if($request->user(), 404);
abort_unless((bool) config_cache('pixelfed.open_registration'), 404);
abort_unless((bool) config_cache('pixelfed.allow_app_registration'), 404);
abort_unless(config_cache('pixelfed.open_registration'), 404);
abort_unless(config('pixelfed.allow_app_registration'), 404);
abort_unless($request->hasHeader('X-PIXELFED-APP'), 403);
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
abort_if(BouncerService::checkIp($request->ip()), 404);
}
$rl = RateLimiter::attempt('pf:apiv1.1:iar:'.$request->ip(), config('pixelfed.app_registration_rate_limit_attempts', 3), function () {
}, config('pixelfed.app_registration_rate_limit_decay', 1800));
$rl = RateLimiter::attempt('pf:apiv1.1:iar:'.$request->ip(), 3, function(){}, 1800);
abort_if(!$rl, 400, 'Too many requests');
$this->validate($request, [
@ -561,10 +542,10 @@ class ApiV1Dot1Controller extends Controller
$user->password = Hash::make($password);
$user->register_source = 'app';
$user->app_register_ip = $request->ip();
$user->app_register_token = Str::random(40);
$user->app_register_token = Str::random(32);
$user->save();
$rtoken = Str::random(64);
$rtoken = Str::random(mt_rand(64, 70));
$verify = new EmailVerification();
$verify->user_id = $user->id;
@ -573,12 +554,7 @@ class ApiV1Dot1Controller extends Controller
$verify->random_token = $rtoken;
$verify->save();
$params = http_build_query([
'ut' => $user->app_register_token,
'rt' => $rtoken,
'ea' => base64_encode($user->email),
]);
$appUrl = url('/api/v1.1/auth/iarer?'.$params);
$appUrl = url('/api/v1.1/auth/iarer?ut=' . $user->app_register_token . '&rt=' . $rtoken);
Mail::to($user->email)->send(new ConfirmAppEmail($verify, $appUrl));
@ -591,41 +567,34 @@ class ApiV1Dot1Controller extends Controller
{
$this->validate($request, [
'ut' => 'required',
'rt' => 'required',
'ea' => 'required',
'rt' => 'required'
]);
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
abort_if(BouncerService::checkIp($request->ip()), 404);
}
$ut = $request->input('ut');
$rt = $request->input('rt');
$ea = $request->input('ea');
$params = http_build_query([
'ut' => $ut,
'rt' => $rt,
'domain' => config('pixelfed.domain.app'),
'ea' => $ea,
]);
$url = 'pixelfed://confirm-account/'.$ut.'?'.$params;
$url = 'pixelfed://confirm-account/'. $ut . '?rt=' . $rt;
return redirect()->away($url);
}
public function inAppRegistrationConfirm(Request $request)
{
abort_if($request->user(), 404);
abort_unless((bool) config_cache('pixelfed.open_registration'), 404);
abort_unless((bool) config_cache('pixelfed.allow_app_registration'), 404);
abort_unless(config_cache('pixelfed.open_registration'), 404);
abort_unless(config('pixelfed.allow_app_registration'), 404);
abort_unless($request->hasHeader('X-PIXELFED-APP'), 403);
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
abort_if(BouncerService::checkIp($request->ip()), 404);
}
$rl = RateLimiter::attempt('pf:apiv1.1:iarc:'.$request->ip(), config('pixelfed.app_registration_confirm_rate_limit_attempts', 20), function () {
}, config('pixelfed.app_registration_confirm_rate_limit_decay', 1800));
abort_if(! $rl, 429, 'Too many requests');
$rl = RateLimiter::attempt('pf:apiv1.1:iarc:'.$request->ip(), 10, function(){}, 1800);
abort_if(!$rl, 400, 'Too many requests');
$request->validate([
$this->validate($request, [
'user_token' => 'required',
'random_token' => 'required',
'email' => 'required',
'email' => 'required'
]);
$verify = EmailVerification::whereEmail($request->input('email'))
@ -639,7 +608,6 @@ class ApiV1Dot1Controller extends Controller
if($verify->created_at->lt(now()->subHours(24))) {
$verify->delete();
return response()->json(['error' => 'Invalid tokens'], 403);
}
@ -648,17 +616,16 @@ class ApiV1Dot1Controller extends Controller
$user->last_active_at = now();
$user->save();
$token = $user->createToken('Pixelfed', ['read', 'write', 'follow', 'admin:read', 'admin:write', 'push']);
$token = $user->createToken('Pixelfed');
return response()->json([
'access_token' => $token->accessToken,
'access_token' => $token->accessToken
]);
}
public function archive(Request $request, $id)
{
abort_if(! $request->user() || ! $request->user()->token(), 403);
abort_unless($request->user()->tokenCan('write'), 403);
abort_if(!$request->user(), 403);
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
abort_if(BouncerService::checkIp($request->ip()), 404);
@ -690,8 +657,7 @@ class ApiV1Dot1Controller extends Controller
public function unarchive(Request $request, $id)
{
abort_if(! $request->user() || ! $request->user()->token(), 403);
abort_unless($request->user()->tokenCan('write'), 403);
abort_if(!$request->user(), 403);
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
abort_if(BouncerService::checkIp($request->ip()), 404);
@ -722,8 +688,7 @@ class ApiV1Dot1Controller extends Controller
public function archivedPosts(Request $request)
{
abort_if(! $request->user() || ! $request->user()->token(), 403);
abort_unless($request->user()->tokenCan('read'), 403);
abort_if(!$request->user(), 403);
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
abort_if(BouncerService::checkIp($request->ip()), 404);
@ -739,8 +704,7 @@ class ApiV1Dot1Controller extends Controller
public function placesById(Request $request, $id, $slug)
{
abort_if(! $request->user() || ! $request->user()->token(), 403);
abort_unless($request->user()->tokenCan('read'), 403);
abort_if(!$request->user(), 403);
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
abort_if(BouncerService::checkIp($request->ip()), 404);
@ -764,29 +728,29 @@ class ApiV1Dot1Controller extends Controller
->values();
return [
'place' => [
'place' =>
[
'id' => $place->id,
'name' => $place->name,
'slug' => $place->slug,
'country' => $place->country,
'lat' => $place->lat,
'long' => $place->long,
'long' => $place->long
],
'posts' => $posts];
}
public function moderatePost(Request $request, $id)
{
abort_if(! $request->user() || ! $request->user()->token(), 403);
abort_if(!$request->user(), 403);
abort_if($request->user()->is_admin != true, 403);
abort_unless($request->user()->tokenCan('admin:write'), 403);
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
abort_if(BouncerService::checkIp($request->ip()), 404);
}
$this->validate($request, [
'action' => 'required|in:cw,mark-public,mark-unlisted,mark-private,mark-spammer,delete',
'action' => 'required|in:cw,mark-public,mark-unlisted,mark-private,mark-spammer,delete'
]);
$action = $request->input('action');
@ -806,7 +770,7 @@ class ApiV1Dot1Controller extends Controller
$status->profile->update([
'unlisted' => true,
'cw' => true,
'no_autolink' => true,
'no_autolink' => true
]);
Status::whereProfileId($status->profile_id)
@ -874,8 +838,7 @@ class ApiV1Dot1Controller extends Controller
Cache::forget('profile:embed:' . $status->profile_id);
StatusService::del($status->id, true);
Cache::forget('profile:status_count:'.$status->profile_id);
$status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status);
StatusDelete::dispatch($status);
return [];
}
@ -886,33 +849,28 @@ class ApiV1Dot1Controller extends Controller
public function getWebSettings(Request $request)
{
abort_if(! $request->user() || ! $request->user()->token(), 403);
abort_unless($request->user()->tokenCan('read'), 403);
abort_if(!$request->user(), 403);
$uid = $request->user()->id;
$settings = UserSetting::firstOrCreate([
'user_id' => $uid,
'user_id' => $uid
]);
if(!$settings->other) {
return [];
}
return $settings->other;
}
public function setWebSettings(Request $request)
{
abort_if(! $request->user() || ! $request->user()->token(), 403);
abort_unless($request->user()->tokenCan('write'), 403);
abort_if(!$request->user(), 403);
$this->validate($request, [
'field' => 'required|in:enable_reblogs,hide_reblog_banner',
'value' => 'required',
'value' => 'required'
]);
$field = $request->input('field');
$value = $request->input('value');
$settings = UserSetting::firstOrCreate([
'user_id' => $request->user()->id,
'user_id' => $request->user()->id
]);
if(!$settings->other) {
$other = [];
@ -925,24 +883,4 @@ class ApiV1Dot1Controller extends Controller
return [200];
}
public function getMutualAccounts(Request $request, $id)
{
abort_if(! $request->user() || ! $request->user()->token(), 403);
abort_unless($request->user()->tokenCan('follows'), 403);
$account = AccountService::get($id, true);
if (! $account || ! isset($account['id'])) {
return [];
}
$res = collect(FollowerService::mutualAccounts($request->user()->profile_id, $id))
->map(function ($accountId) {
return AccountService::get($accountId, true);
})
->filter()
->take(24)
->values();
return $this->json($res);
}
}

View File

@ -34,8 +34,6 @@ use App\Transformer\Api\Mastodon\v1\{
use App\Transformer\Api\{
RelationshipTransformer,
};
use App\Util\Site\Nodeinfo;
use App\Services\UserRoleService;
class ApiV2Controller extends Controller
{
@ -71,16 +69,20 @@ class ApiV2Controller extends Controller
->toArray() : [];
});
$res = Cache::remember('api:v2:instance-data-response-v2', 1800, function () use($contact, $rules) {
return [
$res = [
'domain' => config('pixelfed.domain.app'),
'title' => config_cache('app.name'),
'version' => '3.5.3 (compatible; Pixelfed ' . config('pixelfed.version') .')',
'version' => config('pixelfed.version'),
'source_url' => 'https://github.com/pixelfed/pixelfed',
'description' => config_cache('app.short_description'),
'usage' => [
'users' => [
'active_month' => (int) Nodeinfo::activeUsersMonthly()
'active_month' => (int) Cache::remember('api:nodeinfo:am', 172800, function() {
return User::select('last_active_at', 'created_at')
->where('last_active_at', '>', now()->subMonths(1))
->orWhere('created_at', '>', now()->subMonths(1))
->count();
})
]
],
'thumbnail' => [
@ -94,17 +96,14 @@ class ApiV2Controller extends Controller
'languages' => [config('app.locale')],
'configuration' => [
'urls' => [
'streaming' => null,
'streaming' => 'wss://' . config('pixelfed.domain.app'),
'status' => null
],
'vapid' => [
'public_key' => config('webpush.vapid.public_key'),
],
'accounts' => [
'max_featured_tags' => 0,
],
'statuses' => [
'max_characters' => (int) config_cache('pixelfed.max_caption_length'),
'max_characters' => (int) config('pixelfed.max_caption_length'),
'max_media_attachments' => (int) config_cache('pixelfed.max_album_length'),
'characters_reserved_per_url' => 23
],
@ -117,20 +116,19 @@ class ApiV2Controller extends Controller
'video_matrix_limit' => 3686400
],
'polls' => [
'max_options' => 0,
'max_characters_per_option' => 0,
'min_expiration' => 0,
'max_expiration' => 0,
'max_options' => 4,
'max_characters_per_option' => 50,
'min_expiration' => 300,
'max_expiration' => 2629746,
],
'translation' => [
'enabled' => false,
],
],
'registrations' => [
'enabled' => null,
'enabled' => (bool) config_cache('pixelfed.open_registration'),
'approval_required' => false,
'message' => null,
'url' => null,
'message' => null
],
'contact' => [
'email' => config('instance.email'),
@ -138,10 +136,7 @@ class ApiV2Controller extends Controller
],
'rules' => $rules
];
});
$res['registrations']['enabled'] = (bool) config_cache('pixelfed.open_registration');
$res['registrations']['approval_required'] = (bool) config_cache('instance.curated_registration.enabled');
return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES);
}
@ -153,8 +148,7 @@ class ApiV2Controller extends Controller
*/
public function search(Request $request)
{
abort_if(!$request->user() || !$request->user()->token(), 403);
abort_unless($request->user()->tokenCan('read'), 403);
abort_if(!$request->user(), 403);
$this->validate($request, [
'q' => 'required|string|min:1|max:100',
@ -169,14 +163,6 @@ class ApiV2Controller extends Controller
'following' => 'nullable'
]);
if($request->user()->has_roles && !UserRoleService::can('can-view-discover', $request->user()->id)) {
return [
'accounts' => [],
'hashtags' => [],
'statuses' => []
];
}
$mastodonMode = !$request->has('_pe');
return $this->json(SearchApiV2Service::query($request, $mastodonMode));
}
@ -205,8 +191,7 @@ class ApiV2Controller extends Controller
*/
public function mediaUploadV2(Request $request)
{
abort_if(!$request->user() || !$request->user()->token(), 403);
abort_unless($request->user()->tokenCan('write'), 403);
abort_if(!$request->user(), 403);
$this->validate($request, [
'file.*' => [

View File

@ -99,7 +99,6 @@ class BaseApiController extends Controller
public function avatarUpdate(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'upload' => 'required|mimetypes:image/jpeg,image/jpg,image/png|max:'.config('pixelfed.max_avatar_size'),
]);
@ -135,9 +134,8 @@ class BaseApiController extends Controller
public function verifyCredentials(Request $request)
{
abort_if(!$request->user(), 403);
$user = $request->user();
abort_if(!$user, 403);
if($user->status != null) {
Auth::logout();
abort(403);
@ -149,7 +147,6 @@ class BaseApiController extends Controller
public function accountLikes(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'page' => 'sometimes|int|min:1|max:20',
'limit' => 'sometimes|int|min:1|max:10'

View File

@ -1,119 +0,0 @@
<?php
namespace App\Http\Controllers\Api\V1;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\UserDomainBlock;
use App\Util\ActivityPub\Helpers;
use App\Services\UserFilterService;
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Cache;
use App\Jobs\HomeFeedPipeline\FeedRemoveDomainPipeline;
use App\Jobs\ProfilePipeline\ProfilePurgeNotificationsByDomain;
use App\Jobs\ProfilePipeline\ProfilePurgeFollowersByDomain;
class DomainBlockController extends Controller
{
public function json($res, $code = 200, $headers = [])
{
return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES);
}
public function index(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'limit' => 'sometimes|integer|min:1|max:200'
]);
$limit = $request->input('limit', 100);
$id = $request->user()->profile_id;
$filters = UserDomainBlock::whereProfileId($id)->orderByDesc('id')->cursorPaginate($limit);
$links = null;
$headers = [];
if($filters->nextCursor()) {
$links .= '<'.$filters->nextPageUrl().'&limit='.$limit.'>; rel="next"';
}
if($filters->previousCursor()) {
if($links != null) {
$links .= ', ';
}
$links .= '<'.$filters->previousPageUrl().'&limit='.$limit.'>; rel="prev"';
}
if($links) {
$headers = ['Link' => $links];
}
return $this->json($filters->pluck('domain'), 200, $headers);
}
public function store(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'domain' => 'required|active_url|min:1|max:120'
]);
$pid = $request->user()->profile_id;
$domain = trim($request->input('domain'));
if(Helpers::validateUrl($domain) == false) {
return abort(500, 'Invalid domain or already blocked by server admins');
}
$domain = strtolower(parse_url($domain, PHP_URL_HOST));
abort_if(config_cache('pixelfed.domain.app') == $domain, 400, 'Cannot ban your own server');
$existingCount = UserDomainBlock::whereProfileId($pid)->count();
$maxLimit = (int) config_cache('instance.user_filters.max_domain_blocks');
$errorMsg = __('profile.block.domain.max', ['max' => $maxLimit]);
abort_if($existingCount >= $maxLimit, 400, $errorMsg);
$block = UserDomainBlock::updateOrCreate([
'profile_id' => $pid,
'domain' => $domain
]);
if($block->wasRecentlyCreated) {
Bus::batch([
[
new FeedRemoveDomainPipeline($pid, $domain),
new ProfilePurgeNotificationsByDomain($pid, $domain),
new ProfilePurgeFollowersByDomain($pid, $domain)
]
])->allowFailures()->onQueue('feed')->dispatch();
Cache::forget('profile:following:' . $pid);
UserFilterService::domainBlocks($pid, true);
}
return $this->json([]);
}
public function delete(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'domain' => 'required|min:1|max:120'
]);
$pid = $request->user()->profile_id;
$domain = strtolower(trim($request->input('domain')));
$filters = UserDomainBlock::whereProfileId($pid)->whereDomain($domain)->delete();
UserFilterService::domainBlocks($pid, true);
return $this->json([]);
}
}

View File

@ -1,209 +0,0 @@
<?php
namespace App\Http\Controllers\Api\V1;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Hashtag;
use App\HashtagFollow;
use App\StatusHashtag;
use App\Services\AccountService;
use App\Services\HashtagService;
use App\Services\HashtagFollowService;
use App\Services\HashtagRelatedService;
use App\Http\Resources\MastoApi\FollowedTagResource;
use App\Jobs\HomeFeedPipeline\FeedWarmCachePipeline;
use App\Jobs\HomeFeedPipeline\HashtagUnfollowPipeline;
class TagsController extends Controller
{
const PF_API_ENTITY_KEY = "_pe";
public function json($res, $code = 200, $headers = [])
{
return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES);
}
/**
* GET /api/v1/tags/:id/related
*
*
* @return array
*/
public function relatedTags(Request $request, $tag)
{
abort_if(!$request->user() || !$request->user()->token(), 403);
abort_unless($request->user()->tokenCan('read'), 403);
$tag = Hashtag::whereSlug($tag)->firstOrFail();
return HashtagRelatedService::get($tag->id);
}
/**
* POST /api/v1/tags/:id/follow
*
*
* @return object
*/
public function followHashtag(Request $request, $id)
{
abort_if(!$request->user(), 403);
$pid = $request->user()->profile_id;
$account = AccountService::get($pid);
$operator = config('database.default') == 'pgsql' ? 'ilike' : 'like';
$tag = Hashtag::where('name', $operator, $id)
->orWhere('slug', $operator, $id)
->first();
abort_if(!$tag, 422, 'Unknown hashtag');
abort_if(
HashtagFollow::whereProfileId($pid)->count() >= HashtagFollow::MAX_LIMIT,
422,
'You cannot follow more than ' . HashtagFollow::MAX_LIMIT . ' hashtags.'
);
$follows = HashtagFollow::updateOrCreate(
[
'profile_id' => $account['id'],
'hashtag_id' => $tag->id
],
[
'user_id' => $request->user()->id
]
);
HashtagService::follow($pid, $tag->id);
HashtagFollowService::add($tag->id, $pid);
return response()->json(FollowedTagResource::make($follows)->toArray($request));
}
/**
* POST /api/v1/tags/:id/unfollow
*
*
* @return object
*/
public function unfollowHashtag(Request $request, $id)
{
abort_if(!$request->user(), 403);
$pid = $request->user()->profile_id;
$account = AccountService::get($pid);
$operator = config('database.default') == 'pgsql' ? 'ilike' : 'like';
$tag = Hashtag::where('name', $operator, $id)
->orWhere('slug', $operator, $id)
->first();
abort_if(!$tag, 422, 'Unknown hashtag');
$follows = HashtagFollow::whereProfileId($pid)
->whereHashtagId($tag->id)
->first();
if(!$follows) {
return [
'name' => $tag->name,
'url' => config('app.url') . '/i/web/hashtag/' . $tag->slug,
'history' => [],
'following' => false
];
}
if($follows) {
HashtagService::unfollow($pid, $tag->id);
HashtagFollowService::unfollow($tag->id, $pid);
HashtagUnfollowPipeline::dispatch($tag->id, $pid, $tag->slug)->onQueue('feed');
$follows->delete();
}
$res = FollowedTagResource::make($follows)->toArray($request);
$res['following'] = false;
return response()->json($res);
}
/**
* GET /api/v1/tags/:id
*
*
* @return object
*/
public function getHashtag(Request $request, $id)
{
abort_if(!$request->user(), 403);
$pid = $request->user()->profile_id;
$account = AccountService::get($pid);
$operator = config('database.default') == 'pgsql' ? 'ilike' : 'like';
$tag = Hashtag::where('name', $operator, $id)
->orWhere('slug', $operator, $id)
->first();
if(!$tag) {
return [
'name' => $id,
'url' => config('app.url') . '/i/web/hashtag/' . $id,
'history' => [],
'following' => false
];
}
$res = [
'name' => $tag->name,
'url' => config('app.url') . '/i/web/hashtag/' . $tag->slug,
'history' => [],
'following' => HashtagService::isFollowing($pid, $tag->id)
];
if($request->has(self::PF_API_ENTITY_KEY)) {
$res['count'] = HashtagService::count($tag->id);
}
return $this->json($res);
}
/**
* GET /api/v1/followed_tags
*
*
* @return array
*/
public function getFollowedTags(Request $request)
{
abort_if(!$request->user(), 403);
$account = AccountService::get($request->user()->profile_id);
$this->validate($request, [
'cursor' => 'sometimes',
'limit' => 'sometimes|integer|min:1|max:200'
]);
$limit = $request->input('limit', 100);
$res = HashtagFollow::whereProfileId($account['id'])
->orderByDesc('id')
->cursorPaginate($limit)
->withQueryString();
$pagination = false;
$prevPage = $res->nextPageUrl();
$nextPage = $res->previousPageUrl();
if($nextPage && $prevPage) {
$pagination = '<' . $nextPage . '>; rel="next", <' . $prevPage . '>; rel="prev"';
} else if($nextPage && !$prevPage) {
$pagination = '<' . $nextPage . '>; rel="next"';
} else if(!$nextPage && $prevPage) {
$pagination = '<' . $prevPage . '>; rel="prev"';
}
if($pagination) {
return response()->json(FollowedTagResource::collection($res)->collection)
->header('Link', $pagination);
}
return response()->json(FollowedTagResource::collection($res)->collection);
}
}

View File

@ -62,7 +62,7 @@ class ForgotPasswordController extends Controller
usleep(random_int(100000, 3000000));
if((bool) config_cache('captcha.enabled')) {
if(config('captcha.enabled')) {
$rules = [
'email' => 'required|email',
'h-captcha-response' => 'required|captcha'

View File

@ -71,21 +71,20 @@ class LoginController extends Controller
$this->username() => 'required|email',
'password' => 'required|string|min:6',
];
$messages = [];
if(
(bool) config_cache('captcha.enabled') &&
(bool) config_cache('captcha.active.login') ||
config('captcha.enabled') ||
config('captcha.active.login') ||
(
(bool) config_cache('captcha.triggers.login.enabled') &&
config('captcha.triggers.login.enabled') &&
request()->session()->has('login_attempts') &&
request()->session()->get('login_attempts') >= config('captcha.triggers.login.attempts')
)
) {
$rules['h-captcha-response'] = 'required|filled|captcha|min:5';
$messages['h-captcha-response.required'] = 'The captcha must be filled';
}
$request->validate($rules, $messages);
$this->validate($request, $rules);
}
/**
@ -109,8 +108,8 @@ class LoginController extends Controller
$log->action = 'auth.login';
$log->message = 'Account Login';
$log->link = null;
$log->ip_address = $request->ip();
$log->user_agent = $request->userAgent();
$log->ip_address = "127.0.0.23";
$log->user_agent = "Pixelfed.de";
$log->save();
}

View File

@ -60,7 +60,7 @@ class RegisterController extends Controller
*
* @return \Illuminate\Contracts\Validation\Validator
*/
public function validator(array $data)
protected function validator(array $data)
{
if(config('database.default') == 'pgsql') {
$data['username'] = strtolower($data['username']);
@ -70,7 +70,7 @@ class RegisterController extends Controller
$usernameRules = [
'required',
'min:2',
'max:15',
'max:30',
'unique:users',
function ($attribute, $value, $fail) {
$dash = substr_count($value, '-');
@ -137,7 +137,7 @@ class RegisterController extends Controller
'password' => 'required|string|min:'.config('pixelfed.min_password_length').'|confirmed',
];
if((bool) config_cache('captcha.enabled') && (bool) config_cache('captcha.active.register')) {
if(config('captcha.enabled') || config('captcha.active.register')) {
$rules['h-captcha-response'] = 'required|captcha';
}
@ -151,7 +151,7 @@ class RegisterController extends Controller
*
* @return \App\User
*/
public function create(array $data)
protected function create(array $data)
{
if(config('database.default') == 'pgsql') {
$data['username'] = strtolower($data['username']);
@ -174,7 +174,7 @@ class RegisterController extends Controller
*/
public function showRegistrationForm()
{
if((bool) config_cache('pixelfed.open_registration')) {
if(config_cache('pixelfed.open_registration')) {
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
abort_if(BouncerService::checkIp(request()->ip()), 404);
}
@ -190,14 +190,10 @@ class RegisterController extends Controller
} else {
return view('auth.register');
}
} else {
if((bool) config_cache('instance.curated_registration.enabled') && config('instance.curated_registration.state.fallback_on_closed_reg')) {
return redirect('/auth/sign_up');
} else {
abort(404);
}
}
}
/**
* Handle a registration request for the application.

View File

@ -50,7 +50,7 @@ class ResetPasswordController extends Controller
{
usleep(random_int(100000, 3000000));
if((bool) config_cache('captcha.enabled')) {
if(config('captcha.enabled')) {
return [
'token' => 'required',
'email' => 'required|email',

View File

@ -8,7 +8,6 @@ use Auth;
use Illuminate\Http\Request;
use App\Services\BookmarkService;
use App\Services\FollowerService;
use App\Services\UserRoleService;
class BookmarkController extends Controller
{
@ -23,18 +22,17 @@ class BookmarkController extends Controller
'item' => 'required|integer|min:1',
]);
$user = $request->user();
$profile = Auth::user()->profile;
$status = Status::findOrFail($request->input('item'));
abort_if($user->has_roles && !UserRoleService::can('can-bookmark', $user->id), 403, 'Invalid permissions for this action');
abort_if($status->in_reply_to_id || $status->reblog_of_id, 404);
abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404);
abort_if(!in_array($status->type, ['photo','photo:album', 'video', 'video:album', 'photo:video:album']), 404);
if($status->scope == 'private') {
if($user->profile_id !== $status->profile_id && !FollowerService::follows($user->profile_id, $status->profile_id)) {
if($exists = Bookmark::whereStatusId($status->id)->whereProfileId($user->profile_id)->first()) {
BookmarkService::del($user->profile_id, $status->id);
if($profile->id !== $status->profile_id && !FollowerService::follows($profile->id, $status->profile_id)) {
if($exists = Bookmark::whereStatusId($status->id)->whereProfileId($profile->id)->first()) {
BookmarkService::del($profile->id, $status->id);
$exists->delete();
if ($request->ajax()) {
@ -48,16 +46,22 @@ class BookmarkController extends Controller
}
$bookmark = Bookmark::firstOrCreate(
['status_id' => $status->id], ['profile_id' => $user->profile_id]
['status_id' => $status->id], ['profile_id' => $profile->id]
);
if (!$bookmark->wasRecentlyCreated) {
BookmarkService::del($user->profile_id, $status->id);
BookmarkService::del($profile->id, $status->id);
$bookmark->delete();
} else {
BookmarkService::add($user->profile_id, $status->id);
BookmarkService::add($profile->id, $status->id);
}
return $request->expectsJson() ? ['code' => 200, 'msg' => 'Bookmark saved!'] : redirect()->back();
if ($request->ajax()) {
$response = ['code' => 200, 'msg' => 'Bookmark saved!'];
} else {
$response = redirect()->back();
}
return $response;
}
}

View File

@ -2,15 +2,25 @@
namespace App\Http\Controllers;
use App\Collection;
use App\CollectionItem;
use Illuminate\Http\Request;
use Auth;
use App\{
Collection,
CollectionItem,
Profile,
Status
};
use League\Fractal;
use App\Transformer\Api\{
AccountTransformer,
StatusTransformer,
};
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use App\Services\AccountService;
use App\Services\CollectionService;
use App\Services\FollowerService;
use App\Services\StatusService;
use App\Status;
use Auth;
use Illuminate\Http\Request;
class CollectionController extends Controller
{
@ -21,11 +31,10 @@ class CollectionController extends Controller
$collection = Collection::firstOrCreate([
'profile_id' => $profile->id,
'published_at' => null,
'published_at' => null
]);
$collection->visibility = 'draft';
$collection->save();
return view('collection.create', compact('collection'));
}
@ -43,14 +52,12 @@ class CollectionController extends Controller
}
}
}
return view('collection.show', compact('collection'));
}
public function index(Request $request)
{
abort_if(!Auth::check(), 403);
return $request->all();
}
@ -60,7 +67,7 @@ class CollectionController extends Controller
$this->validate($request, [
'title' => 'nullable|max:50',
'description' => 'nullable|max:500',
'visibility' => 'nullable|string|in:public,private,draft',
'visibility' => 'nullable|string|in:public,private,draft'
]);
$pid = $request->user()->profile_id;
@ -71,7 +78,6 @@ class CollectionController extends Controller
$collection->save();
CollectionService::deleteCollection($id);
return CollectionService::setCollection($collection->id, $collection);
}
@ -81,7 +87,7 @@ class CollectionController extends Controller
$this->validate($request, [
'title' => 'nullable|max:50',
'description' => 'nullable|max:500',
'visibility' => 'required|alpha|in:public,private,draft',
'visibility' => 'required|alpha|in:public,private,draft'
]);
$profile = Auth::user()->profile;
$collection = Collection::whereProfileId($profile->id)->findOrFail($id);
@ -93,7 +99,6 @@ class CollectionController extends Controller
$collection->visibility = $request->input('visibility');
$collection->published_at = now();
$collection->save();
return CollectionService::setCollection($collection->id, $collection);
}
@ -121,7 +126,7 @@ class CollectionController extends Controller
$this->validate($request, [
'collection_id' => 'required|int|min:1|exists:collections,id',
'post_id' => 'required|int|min:1',
'post_id' => 'required|int|min:1'
]);
$profileId = $request->user()->profile_id;
@ -148,7 +153,7 @@ class CollectionController extends Controller
abort(400, 'You can only add '.$max.' posts per collection');
}
$status = Status::whereIn('scope', ['public', 'unlisted'])
$status = Status::whereScope('public')
->whereProfileId($profileId)
->whereIn('type', ['photo', 'photo:album', 'video'])
->findOrFail($postId);
@ -156,18 +161,22 @@ class CollectionController extends Controller
$item = CollectionItem::firstOrCreate([
'collection_id' => $collection->id,
'object_type' => 'App\Status',
'object_id' => $status->id,
'object_id' => $status->id
],[
'order' => $count,
]);
CollectionService::deleteCollection($collection->id);
CollectionService::addItem(
$collection->id,
$status->id,
$count
);
$collection->updated_at = now();
$collection->save();
CollectionService::setCollection($collection->id, $collection);
return StatusService::get($status->id, false);
return StatusService::get($status->id);
}
public function getCollection(Request $request, $id)
@ -217,10 +226,10 @@ class CollectionController extends Controller
return collect($items)
->map(function($id) {
return StatusService::get($id, false);
return StatusService::get($id);
})
->filter(function($item) {
return $item && ($item['visibility'] == 'public' || $item['visibility'] == 'unlisted') && isset($item['account'], $item['media_attachments']);
return $item && isset($item['account'], $item['media_attachments']);
})
->values();
}
@ -275,7 +284,7 @@ class CollectionController extends Controller
abort_if(!$request->user(), 403);
$this->validate($request, [
'collection_id' => 'required|int|min:1|exists:collections,id',
'post_id' => 'required|int|min:1',
'post_id' => 'required|int|min:1'
]);
$profileId = $request->user()->profile_id;
@ -289,7 +298,7 @@ class CollectionController extends Controller
abort(400, 'You cannot delete the only post of a collection!');
}
$status = Status::whereIn('scope', ['public', 'unlisted'])
$status = Status::whereScope('public')
->whereIn('type', ['photo', 'photo:album', 'video'])
->findOrFail($postId);
@ -314,31 +323,4 @@ class CollectionController extends Controller
return 200;
}
public function getSelfCollections(Request $request)
{
abort_if(! $request->user(), 404);
$user = $request->user();
$pid = $user->profile_id;
$profile = AccountService::get($pid, true);
if (! $profile || ! isset($profile['id'])) {
return response()->json([], 404);
}
return Collection::whereProfileId($pid)
->orderByDesc('id')
->paginate(9)
->map(function ($collection) {
$c = CollectionService::getCollection($collection->id);
$c['items'] = collect(CollectionService::getItems($collection->id))
->map(function ($id) {
return StatusService::get($id, false);
})->filter()->values();
return $c;
})
->filter()
->values();
}
}

View File

@ -2,18 +2,23 @@
namespace App\Http\Controllers;
use App\Jobs\CommentPipeline\CommentPipeline;
use App\Jobs\StatusPipeline\NewStatusPipeline;
use App\Services\StatusService;
use App\Status;
use App\Transformer\Api\StatusTransformer;
use App\UserFilter;
use App\Util\Lexer\Autolink;
use Illuminate\Http\Request;
use Auth;
use DB;
use Illuminate\Http\Request;
use Cache;
use App\Comment;
use App\Jobs\CommentPipeline\CommentPipeline;
use App\Jobs\StatusPipeline\NewStatusPipeline;
use App\Util\Lexer\Autolink;
use App\Profile;
use App\Status;
use App\UserFilter;
use League\Fractal;
use App\Transformer\Api\StatusTransformer;
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use App\Services\StatusService;
class CommentController extends Controller
{
@ -29,8 +34,8 @@ class CommentController extends Controller
}
$this->validate($request, [
'item' => 'required|integer|min:1',
'comment' => 'required|string|max:'.config_cache('pixelfed.max_caption_length'),
'sensitive' => 'nullable|boolean',
'comment' => 'required|string|max:'.(int) config('pixelfed.max_caption_length'),
'sensitive' => 'nullable|boolean'
]);
$comment = $request->input('comment');
$statusId = $request->input('item');

View File

@ -2,38 +2,58 @@
namespace App\Http\Controllers;
use App\Collection;
use App\CollectionItem;
use App\Hashtag;
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
use App\Jobs\StatusPipeline\NewStatusPipeline;
use App\Jobs\VideoPipeline\VideoThumbnail;
use App\Media;
use App\MediaTag;
use Illuminate\Http\Request;
use Auth, Cache, DB, Storage, URL;
use Carbon\Carbon;
use App\{
Avatar,
Collection,
CollectionItem,
Hashtag,
Like,
Media,
MediaTag,
Notification,
Profile,
Place,
Status,
UserFilter,
UserSetting
};
use App\Models\Poll;
use App\Notification;
use App\Profile;
use App\Transformer\Api\{
MediaTransformer,
MediaDraftTransformer,
StatusTransformer,
StatusStatelessTransformer
};
use League\Fractal;
use App\Util\Media\Filter;
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use App\Jobs\AvatarPipeline\AvatarOptimize;
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
use App\Jobs\ImageOptimizePipeline\ImageThumbnail;
use App\Jobs\StatusPipeline\NewStatusPipeline;
use App\Jobs\VideoPipeline\{
VideoOptimize,
VideoPostProcess,
VideoThumbnail
};
use App\Services\AccountService;
use App\Services\CollectionService;
use App\Services\MediaBlocklistService;
use App\Services\NotificationService;
use App\Services\MediaPathService;
use App\Services\MediaBlocklistService;
use App\Services\MediaStorageService;
use App\Services\MediaTagService;
use App\Services\StatusService;
use App\Services\SnowflakeService;
use App\Services\UserRoleService;
use App\Status;
use App\Transformer\Api\MediaTransformer;
use App\UserFilter;
use App\Util\Lexer\Autolink;
use App\Util\Media\Filter;
use App\Util\Media\License;
use Auth;
use Cache;
use DB;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
use App\Util\Lexer\Autolink;
use App\Util\Lexer\Extractor;
use App\Util\Media\License;
use Image;
class ComposeController extends Controller
{
@ -67,12 +87,11 @@ class ComposeController extends Controller
'max:' . config_cache('pixelfed.max_photo_size'),
],
'filter_name' => 'nullable|string|max:24',
'filter_class' => 'nullable|alpha_dash|max:24',
'filter_class' => 'nullable|alpha_dash|max:24'
]);
$user = Auth::user();
$profile = $user->profile;
abort_if($user->has_roles && ! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
$limitKey = 'compose:rate-limit:media-upload:' . $user->id;
$limitTtl = now()->addMinutes(15);
@ -148,7 +167,6 @@ class ComposeController extends Controller
$res = $this->fractal->createData($resource)->toArray();
$res['preview_url'] = $preview_url;
$res['url'] = $url;
return response()->json($res);
}
@ -166,7 +184,6 @@ class ComposeController extends Controller
]);
$user = Auth::user();
abort_if($user->has_roles && ! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
$limitKey = 'compose:rate-limit:media-updates:' . $user->id;
$limitTtl = now()->addMinutes(15);
@ -194,11 +211,10 @@ class ComposeController extends Controller
$dir = implode('/', $fragments);
$path = $photo->storePubliclyAs($dir, $name);
$res = [
'url' => $media->url().'?v='.time(),
'url' => $media->url() . '?v=' . time()
];
ImageOptimize::dispatch($media)->onQueue('mmo');
Cache::forget($limitKey);
return $res;
}
@ -207,11 +223,9 @@ class ComposeController extends Controller
abort_if(!$request->user(), 403);
$this->validate($request, [
'id' => 'required|integer|min:1|exists:media,id',
'id' => 'required|integer|min:1|exists:media,id'
]);
abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
$media = Media::whereNull('status_id')
->whereUserId(Auth::id())
->findOrFail($request->input('id'));
@ -220,7 +234,7 @@ class ComposeController extends Controller
return response()->json([
'msg' => 'Successfully deleted',
'code' => 200,
'code' => 200
]);
}
@ -229,7 +243,7 @@ class ComposeController extends Controller
abort_if(!$request->user(), 403);
$this->validate($request, [
'q' => 'required|string|min:1|max:50',
'q' => 'required|string|min:1|max:50'
]);
$q = $request->input('q');
@ -241,10 +255,6 @@ class ComposeController extends Controller
$q = mb_substr($q, 1);
}
$user = $request->user();
abort_if($user->has_roles && ! UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
$blocked = UserFilter::whereFilterableType('App\Profile')
->whereFilterType('block')
->whereFilterableId($request->user()->profile_id)
@ -263,7 +273,7 @@ class ComposeController extends Controller
'id' => (string) $r->id,
'name' => $r->username,
'privacy' => true,
'avatar' => $r->avatarUrl(),
'avatar' => $r->avatarUrl()
];
});
@ -276,11 +286,9 @@ class ComposeController extends Controller
$this->validate($request, [
'status_id' => 'required',
'profile_id' => 'required',
'profile_id' => 'required'
]);
abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
$user = $request->user();
$status_id = $request->input('status_id');
$profile_id = (int) $request->input('profile_id');
@ -309,9 +317,8 @@ class ComposeController extends Controller
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'q' => 'required|string|max:100',
'q' => 'required|string|max:100'
]);
abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
$pid = $request->user()->profile_id;
abort_if(!$pid, 400);
$q = e($request->input('q'));
@ -332,13 +339,12 @@ class ComposeController extends Controller
->map(function($place) {
return [
'id' => $place->place_id,
'count' => $place->pc,
'count' => $place->pc
];
})
->unique('id')
->values();
}
return Status::selectRaw('id, place_id, count(place_id) as pc')
->whereNotNull('place_id')
->where('id', '>', $minId)
@ -352,7 +358,7 @@ class ComposeController extends Controller
->map(function($place) {
return [
'id' => $place->place_id,
'count' => $place->pc,
'count' => $place->pc
];
});
});
@ -375,12 +381,11 @@ class ComposeController extends Controller
'id' => $r->id,
'name' => $r->name,
'country' => $r->country,
'url' => url('/discover/places/'.$r->id.'/'.$r->slug),
'url' => url('/discover/places/' . $r->id . '/' . $r->slug)
];
})
->values()
->all();
return $places;
}
@ -389,11 +394,9 @@ class ComposeController extends Controller
abort_if(!$request->user(), 403);
$this->validate($request, [
'q' => 'required|string|min:2|max:50',
'q' => 'required|string|min:2|max:50'
]);
abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
$q = $request->input('q');
if(Str::of($q)->startsWith('@')) {
@ -412,12 +415,11 @@ class ComposeController extends Controller
$results = Profile::select('id','domain','username')
->whereNotIn('id', $blocked)
->where('username','like','%'.$q.'%')
->groupBy('id', 'domain')
->groupBy('domain')
->limit(15)
->get()
->map(function($profile) {
$username = $profile->domain ? substr($profile->username, 1) : $profile->username;
return [
'key' => '@' . str_limit($username, 30),
'value' => $username,
@ -432,11 +434,9 @@ class ComposeController extends Controller
abort_if(!$request->user(), 403);
$this->validate($request, [
'q' => 'required|string|min:2|max:50',
'q' => 'required|string|min:2|max:50'
]);
abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
$q = $request->input('q');
$results = Hashtag::select('slug')
@ -448,7 +448,7 @@ class ComposeController extends Controller
->map(function($tag) {
return [
'key' => '#' . $tag->slug,
'value' => $tag->slug,
'value' => $tag->slug
];
});
@ -458,7 +458,7 @@ class ComposeController extends Controller
public function store(Request $request)
{
$this->validate($request, [
'caption' => 'nullable|string|max:'.config_cache('pixelfed.max_caption_length', 500),
'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
'media.*' => 'required',
'media.*.id' => 'required|integer|min:1',
'media.*.filter_class' => 'nullable|alpha_dash|max:30',
@ -475,8 +475,6 @@ class ComposeController extends Controller
// 'optimize_media' => 'nullable'
]);
abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
if(config('costar.enabled') == true) {
$blockedKeywords = config('costar.keyword.block');
if($blockedKeywords !== null && $request->caption) {
@ -489,7 +487,7 @@ class ComposeController extends Controller
}
}
$user = $request->user();
$user = Auth::user();
$profile = $user->profile;
$limitKey = 'compose:rate-limit:store:' . $user->id;
@ -604,9 +602,9 @@ class ComposeController extends Controller
CollectionItem::firstOrCreate([
'collection_id' => $collection->id,
'object_type' => 'App\Status',
'object_id' => $status->id,
'object_id' => $status->id
], [
'order' => $count,
'order' => $count
]);
CollectionService::addItem(
@ -637,7 +635,7 @@ class ComposeController extends Controller
{
abort_unless(config('exp.top'), 404);
$this->validate($request, [
'caption' => 'nullable|string|max:'.config_cache('pixelfed.max_caption_length', 500),
'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
'cw' => 'nullable|boolean',
'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10',
'place' => 'nullable',
@ -645,8 +643,6 @@ class ComposeController extends Controller
'tagged' => 'nullable',
]);
abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
if(config('costar.enabled') == true) {
$blockedKeywords = config('costar.keyword.block');
if($blockedKeywords !== null && $request->caption) {
@ -659,7 +655,7 @@ class ComposeController extends Controller
}
}
$user = $request->user();
$user = Auth::user();
$profile = $user->profile;
$visibility = $request->input('visibility');
$status = new Status;
@ -691,7 +687,7 @@ class ComposeController extends Controller
'bg_id' => 1,
'font_size' => strlen($status->caption) <= 140 ? 'h1' : 'h3',
'length' => strlen($status->caption),
],
]
], $entities), JSON_UNESCAPED_SLASHES);
$status->save();
@ -710,6 +706,7 @@ class ComposeController extends Controller
MediaTagService::sendNotification($mt);
}
Cache::forget('user:account:id:'.$profile->user_id);
Cache::forget('_api:statuses:recent_9:'.$profile->id);
Cache::forget('profile:status_count:'.$profile->id);
@ -720,18 +717,16 @@ class ComposeController extends Controller
public function mediaProcessingCheck(Request $request)
{
$this->validate($request, [
'id' => 'required|integer|min:1',
'id' => 'required|integer|min:1'
]);
abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
$media = Media::whereUserId($request->user()->id)
->whereNull('status_id')
->findOrFail($request->input('id'));
if(config('pixelfed.media_fast_process')) {
return [
'finished' => true,
'finished' => true
];
}
@ -741,28 +736,26 @@ class ComposeController extends Controller
case 'image/jpeg':
case 'image/png':
case 'video/mp4':
$finished = (bool) config_cache('pixelfed.cloud_storage') ? (bool) $media->cdn_url : (bool) $media->processed_at;
$finished = config_cache('pixelfed.cloud_storage') ? (bool) $media->cdn_url : (bool) $media->processed_at;
break;
default:
// code...
# code...
break;
}
return [
'finished' => $finished,
'finished' => $finished
];
}
public function composeSettings(Request $request)
{
$uid = $request->user()->id;
abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
$default = [
'default_license' => 1,
'media_descriptions' => false,
'max_altext_length' => config_cache('pixelfed.max_altext_length'),
'max_altext_length' => config_cache('pixelfed.max_altext_length')
];
$settings = AccountService::settings($uid);
if(isset($settings['other']) && isset($settings['other']['scope'])) {
@ -777,22 +770,22 @@ class ComposeController extends Controller
public function createPoll(Request $request)
{
$this->validate($request, [
'caption' => 'nullable|string|max:'.config_cache('pixelfed.max_caption_length', 500),
'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
'cw' => 'nullable|boolean',
'visibility' => 'required|string|in:public,private',
'comments_disabled' => 'nullable',
'expiry' => 'required|in:60,360,1440,10080',
'pollOptions' => 'required|array|min:1|max:4',
'pollOptions' => 'required|array|min:1|max:4'
]);
abort(404);
abort_if(config('instance.polls.enabled') == false, 404, 'Polls not enabled');
abort_if($request->user()->has_roles && ! UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
abort_if(Status::whereType('poll')
->whereProfileId($request->user()->profile_id)
->whereCaption($request->input('caption'))
->where('created_at', '>', now()->subDays(2))
->exists(), 422, 'Duplicate detected.');
->exists()
, 422, 'Duplicate detected.');
$status = new Status;
$status->profile_id = $request->user()->profile_id;

View File

@ -1,399 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use App\User;
use App\Models\CuratedRegister;
use App\Models\CuratedRegisterActivity;
use App\Services\EmailService;
use App\Services\BouncerService;
use App\Util\Lexer\RestrictedNames;
use App\Mail\CuratedRegisterConfirmEmail;
use App\Mail\CuratedRegisterNotifyAdmin;
use Illuminate\Support\Facades\Mail;
use App\Jobs\CuratedOnboarding\CuratedOnboardingNotifyAdminNewApplicationPipeline;
class CuratedRegisterController extends Controller
{
public function __construct()
{
abort_unless((bool) config_cache('instance.curated_registration.enabled'), 404);
if((bool) config_cache('pixelfed.open_registration')) {
abort_if(config('instance.curated_registration.state.only_enabled_on_closed_reg'), 404);
} else {
abort_unless(config('instance.curated_registration.state.fallback_on_closed_reg'), 404);
}
}
public function index(Request $request)
{
abort_if($request->user(), 404);
return view('auth.curated-register.index', ['step' => 1]);
}
public function concierge(Request $request)
{
abort_if($request->user(), 404);
$emailConfirmed = $request->session()->has('cur-reg-con.email-confirmed') &&
$request->has('next') &&
$request->session()->has('cur-reg-con.cr-id');
return view('auth.curated-register.concierge', compact('emailConfirmed'));
}
public function conciergeResponseSent(Request $request)
{
return view('auth.curated-register.user_response_sent');
}
public function conciergeFormShow(Request $request)
{
abort_if($request->user(), 404);
abort_unless(
$request->session()->has('cur-reg-con.email-confirmed') &&
$request->session()->has('cur-reg-con.cr-id') &&
$request->session()->has('cur-reg-con.ac-id'), 404);
$crid = $request->session()->get('cur-reg-con.cr-id');
$arid = $request->session()->get('cur-reg-con.ac-id');
$showCaptcha = config('instance.curated_registration.captcha_enabled');
if($attempts = $request->session()->get('cur-reg-con-attempt')) {
$showCaptcha = $attempts && $attempts >= 2;
} else {
$showCaptcha = false;
}
$activity = CuratedRegisterActivity::whereRegisterId($crid)->whereFromAdmin(true)->findOrFail($arid);
return view('auth.curated-register.concierge_form', compact('activity', 'showCaptcha'));
}
public function conciergeFormStore(Request $request)
{
abort_if($request->user(), 404);
$request->session()->increment('cur-reg-con-attempt');
abort_unless(
$request->session()->has('cur-reg-con.email-confirmed') &&
$request->session()->has('cur-reg-con.cr-id') &&
$request->session()->has('cur-reg-con.ac-id'), 404);
$attempts = $request->session()->get('cur-reg-con-attempt');
$messages = [];
$rules = [
'response' => 'required|string|min:5|max:1000',
'crid' => 'required|integer|min:1',
'acid' => 'required|integer|min:1'
];
if(config('instance.curated_registration.captcha_enabled') && $attempts >= 3) {
$rules['h-captcha-response'] = 'required|captcha';
$messages['h-captcha-response.required'] = 'The captcha must be filled';
}
$this->validate($request, $rules, $messages);
$crid = $request->session()->get('cur-reg-con.cr-id');
$acid = $request->session()->get('cur-reg-con.ac-id');
abort_if((string) $crid !== $request->input('crid'), 404);
abort_if((string) $acid !== $request->input('acid'), 404);
if(CuratedRegisterActivity::whereRegisterId($crid)->whereReplyToId($acid)->exists()) {
return redirect()->back()->withErrors(['code' => 'You already replied to this request.']);
}
$act = CuratedRegisterActivity::create([
'register_id' => $crid,
'reply_to_id' => $acid,
'type' => 'user_response',
'message' => $request->input('response'),
'from_user' => true,
'action_required' => true,
]);
CuratedRegister::findOrFail($crid)->update(['user_has_responded' => true]);
$request->session()->pull('cur-reg-con');
$request->session()->pull('cur-reg-con-attempt');
return view('auth.curated-register.user_response_sent');
}
public function conciergeStore(Request $request)
{
abort_if($request->user(), 404);
$rules = [
'sid' => 'required_if:action,email|integer|min:1|max:20000000',
'id' => 'required_if:action,email|integer|min:1|max:20000000',
'code' => 'required_if:action,email',
'action' => 'required|string|in:email,message',
'email' => 'required_if:action,email|email',
'response' => 'required_if:action,message|string|min:20|max:1000',
];
$messages = [];
if(config('instance.curated_registration.captcha_enabled')) {
$rules['h-captcha-response'] = 'required|captcha';
$messages['h-captcha-response.required'] = 'The captcha must be filled';
}
$this->validate($request, $rules, $messages);
$action = $request->input('action');
$sid = $request->input('sid');
$id = $request->input('id');
$code = $request->input('code');
$email = $request->input('email');
$cr = CuratedRegister::whereIsClosed(false)->findOrFail($sid);
$ac = CuratedRegisterActivity::whereRegisterId($cr->id)->whereFromAdmin(true)->findOrFail($id);
if(!hash_equals($ac->secret_code, $code)) {
return redirect()->back()->withErrors(['code' => 'Invalid code']);
}
if(!hash_equals($cr->email, $email)) {
return redirect()->back()->withErrors(['email' => 'Invalid email']);
}
$request->session()->put('cur-reg-con.email-confirmed', true);
$request->session()->put('cur-reg-con.cr-id', $cr->id);
$request->session()->put('cur-reg-con.ac-id', $ac->id);
$emailConfirmed = true;
return redirect('/auth/sign_up/concierge/form');
}
public function confirmEmail(Request $request)
{
if($request->user()) {
return redirect(route('help.email-confirmation-issues'));
}
return view('auth.curated-register.confirm_email');
}
public function emailConfirmed(Request $request)
{
if($request->user()) {
return redirect(route('help.email-confirmation-issues'));
}
return view('auth.curated-register.email_confirmed');
}
public function resendConfirmation(Request $request)
{
return view('auth.curated-register.resend-confirmation');
}
public function resendConfirmationProcess(Request $request)
{
$rules = [
'email' => [
'required',
'string',
app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email',
'exists:curated_registers',
]
];
$messages = [];
if(config('instance.curated_registration.captcha_enabled')) {
$rules['h-captcha-response'] = 'required|captcha';
$messages['h-captcha-response.required'] = 'The captcha must be filled';
}
$this->validate($request, $rules, $messages);
$cur = CuratedRegister::whereEmail($request->input('email'))->whereIsClosed(false)->first();
if(!$cur) {
return redirect()->back()->withErrors(['email' => 'The selected email is invalid.']);
}
$totalCount = CuratedRegisterActivity::whereRegisterId($cur->id)
->whereType('user_resend_email_confirmation')
->count();
if($totalCount && $totalCount >= config('instance.curated_registration.resend_confirmation_limit')) {
return redirect()->back()->withErrors(['email' => 'You have re-attempted too many times. To proceed with your application, please <a href="/site/contact" class="text-white" style="text-decoration: underline;">contact the admin team</a>.']);
}
$count = CuratedRegisterActivity::whereRegisterId($cur->id)
->whereType('user_resend_email_confirmation')
->where('created_at', '>', now()->subHours(12))
->count();
if($count) {
return redirect()->back()->withErrors(['email' => 'You can only re-send the confirmation email once per 12 hours. Try again later.']);
}
CuratedRegisterActivity::create([
'register_id' => $cur->id,
'type' => 'user_resend_email_confirmation',
'admin_only_view' => true,
'from_admin' => false,
'from_user' => false,
'action_required' => false,
]);
Mail::to($cur->email)->send(new CuratedRegisterConfirmEmail($cur));
return view('auth.curated-register.resent-confirmation');
return $request->all();
}
public function confirmEmailHandle(Request $request)
{
$rules = [
'sid' => 'required',
'code' => 'required'
];
$messages = [];
if(config('instance.curated_registration.captcha_enabled')) {
$rules['h-captcha-response'] = 'required|captcha';
$messages['h-captcha-response.required'] = 'The captcha must be filled';
}
$this->validate($request, $rules, $messages);
$cr = CuratedRegister::whereNull('email_verified_at')
->where('created_at', '>', now()->subHours(24))
->find($request->input('sid'));
if(!$cr) {
return redirect(route('help.email-confirmation-issues'));
}
if(!hash_equals($cr->verify_code, $request->input('code'))) {
return redirect(route('help.email-confirmation-issues'));
}
$cr->email_verified_at = now();
$cr->save();
if(config('instance.curated_registration.notify.admin.on_verify_email.enabled')) {
CuratedOnboardingNotifyAdminNewApplicationPipeline::dispatch($cr);
}
return view('auth.curated-register.email_confirmed');
}
public function proceed(Request $request)
{
$this->validate($request, [
'step' => 'required|integer|in:1,2,3,4'
]);
$step = $request->input('step');
switch($step) {
case 1:
$step = 2;
$request->session()->put('cur-step', 1);
return view('auth.curated-register.index', compact('step'));
break;
case 2:
$this->stepTwo($request);
$step = 3;
$request->session()->put('cur-step', 2);
return view('auth.curated-register.index', compact('step'));
break;
case 3:
$this->stepThree($request);
$step = 3;
$request->session()->put('cur-step', 3);
$verifiedEmail = true;
$request->session()->pull('cur-reg');
return view('auth.curated-register.index', compact('step', 'verifiedEmail'));
break;
}
}
protected function stepTwo($request)
{
if($request->filled('reason')) {
$request->session()->put('cur-reg.form-reason', $request->input('reason'));
}
if($request->filled('username')) {
$request->session()->put('cur-reg.form-username', $request->input('username'));
}
if($request->filled('email')) {
$request->session()->put('cur-reg.form-email', $request->input('email'));
}
$this->validate($request, [
'username' => [
'required',
'min:2',
'max:15',
'unique:curated_registers',
'unique:users',
function ($attribute, $value, $fail) {
$dash = substr_count($value, '-');
$underscore = substr_count($value, '_');
$period = substr_count($value, '.');
if(ends_with($value, ['.php', '.js', '.css'])) {
return $fail('Username is invalid.');
}
if(($dash + $underscore + $period) > 1) {
return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
}
if (!ctype_alnum($value[0])) {
return $fail('Username is invalid. Must start with a letter or number.');
}
if (!ctype_alnum($value[strlen($value) - 1])) {
return $fail('Username is invalid. Must end with a letter or number.');
}
$val = str_replace(['_', '.', '-'], '', $value);
if(!ctype_alnum($val)) {
return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
}
$restricted = RestrictedNames::get();
if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
return $fail('Username cannot be used.');
}
},
],
'email' => [
'required',
'string',
app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email',
'max:255',
'unique:users',
'unique:curated_registers',
function ($attribute, $value, $fail) {
$banned = EmailService::isBanned($value);
if($banned) {
return $fail('Email is invalid.');
}
},
],
'password' => 'required|min:8',
'password_confirmation' => 'required|same:password',
'reason' => 'required|min:20|max:1000',
'agree' => 'required|accepted'
]);
$request->session()->put('cur-reg.form-email', $request->input('email'));
$request->session()->put('cur-reg.form-password', $request->input('password'));
}
protected function stepThree($request)
{
$this->validate($request, [
'email' => [
'required',
'string',
app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email',
'max:255',
'unique:users',
'unique:curated_registers',
function ($attribute, $value, $fail) {
$banned = EmailService::isBanned($value);
if($banned) {
return $fail('Email is invalid.');
}
},
]
]);
$cr = new CuratedRegister;
$cr->email = $request->email;
$cr->username = $request->session()->get('cur-reg.form-username');
$cr->password = bcrypt($request->session()->get('cur-reg.form-password'));
$cr->ip_address = $request->ip();
$cr->reason_to_join = $request->session()->get('cur-reg.form-reason');
$cr->verify_code = Str::random(40);
$cr->save();
Mail::to($cr->email)->send(new CuratedRegisterConfirmEmail($cr));
}
}

View File

@ -17,16 +17,12 @@ use App\{
use App\Services\MediaPathService;
use App\Services\MediaBlocklistService;
use App\Jobs\StatusPipeline\NewStatusPipeline;
use App\Jobs\StatusPipeline\StatusDelete;
use Illuminate\Support\Str;
use App\Util\ActivityPub\Helpers;
use App\Services\AccountService;
use App\Services\StatusService;
use App\Services\WebfingerService;
use App\Models\Conversation;
use App\Jobs\DirectPipeline\DirectDeletePipeline;
use App\Jobs\DirectPipeline\DirectDeliverPipeline;
use App\Services\UserRoleService;
class DirectMessageController extends Controller
{
@ -42,11 +38,7 @@ class DirectMessageController extends Controller
'page' => 'nullable|integer|min:1|max:99'
]);
$user = $request->user();
if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id)) {
return [];
}
$profile = $user->profile_id;
$profile = $request->user()->profile_id;
$action = $request->input('a', 'inbox');
$page = $request->input('page');
@ -307,10 +299,7 @@ class DirectMessageController extends Controller
'type' => 'required|in:text,emoji'
]);
$user = $request->user();
abort_if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action');
abort_if($user->created_at->gt(now()->subHours(72)), 400, 'You need to wait a bit before you can DM another account');
$profile = $user->profile;
$profile = $request->user()->profile;
$recipient = Profile::where('id', '!=', $profile->id)->findOrFail($request->input('to_id'));
abort_if(in_array($profile->id, $recipient->blockedIds()->toArray()), 403);
@ -409,10 +398,7 @@ class DirectMessageController extends Controller
$this->validate($request, [
'pid' => 'required'
]);
$user = $request->user();
abort_if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action');
$uid = $user->profile_id;
$uid = $request->user()->profile_id;
$pid = $request->input('pid');
$max_id = $request->input('max_id');
$min_id = $request->input('min_id');
@ -514,8 +500,6 @@ class DirectMessageController extends Controller
if($recipient['local'] == false) {
$dmc = $dm;
$this->remoteDelete($dmc);
} else {
StatusDelete::dispatch($status)->onQueue('high');
}
if(Conversation::whereStatusId($sid)->count()) {
@ -557,15 +541,14 @@ class DirectMessageController extends Controller
StatusService::del($status->id, true);
$status->forceDeleteQuietly();
$status->delete();
$dm->delete();
return [200];
}
public function get(Request $request, $id)
{
$user = $request->user();
abort_if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action');
$pid = $request->user()->profile_id;
$dm = DirectMessage::whereStatusId($id)->firstOrFail();
abort_if($pid !== $dm->to_id && $pid !== $dm->from_id, 404);
@ -586,7 +569,6 @@ class DirectMessageController extends Controller
]);
$user = $request->user();
abort_if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action');
$profile = $user->profile;
$recipient = Profile::where('id', '!=', $profile->id)->findOrFail($request->input('to_id'));
abort_if(in_array($profile->id, $recipient->blockedIds()->toArray()), 403);
@ -685,11 +667,6 @@ class DirectMessageController extends Controller
'remote' => 'nullable',
]);
$user = $request->user();
if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id)) {
return [];
}
$q = $request->input('q');
$r = $request->input('remote', false);
@ -748,8 +725,6 @@ class DirectMessageController extends Controller
$pid = $request->input('pid');
$sid = $request->input('sid');
$user = $request->user();
abort_if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action');
$dms = DirectMessage::whereToId($request->user()->profile_id)
->whereFromId($pid)
@ -771,8 +746,6 @@ class DirectMessageController extends Controller
'id' => 'required'
]);
$user = $request->user();
abort_if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action');
$fid = $request->input('id');
$pid = $request->user()->profile_id;
@ -794,9 +767,6 @@ class DirectMessageController extends Controller
'id' => 'required'
]);
$user = $request->user();
abort_if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action');
$fid = $request->input('id');
$pid = $request->user()->profile_id;
@ -859,7 +829,7 @@ class DirectMessageController extends Controller
]
];
DirectDeliverPipeline::dispatch($profile, $url, $body)->onQueue('high');
Helpers::sendSignedObject($profile, $url, $body);
}
public function remoteDelete($dm)
@ -882,6 +852,7 @@ class DirectMessageController extends Controller
'type' => 'Tombstone'
]
];
DirectDeletePipeline::dispatch($profile, $url, $body)->onQueue('high');
Helpers::sendSignedObject($profile, $url, $body);
}
}

View File

@ -2,50 +2,48 @@
namespace App\Http\Controllers;
use App\Hashtag;
use App\Instance;
use App\Like;
use App\Services\AccountService;
use App\Services\AdminShadowFilterService;
use App\{
DiscoverCategory,
Follower,
Hashtag,
HashtagFollow,
Instance,
Like,
Profile,
Status,
StatusHashtag,
UserFilter
};
use Auth, DB, Cache;
use Illuminate\Http\Request;
use App\Services\BookmarkService;
use App\Services\ConfigCacheService;
use App\Services\FollowerService;
use App\Services\HashtagService;
use App\Services\LikeService;
use App\Services\ReblogService;
use App\Services\SnowflakeService;
use App\Services\StatusHashtagService;
use App\Services\SnowflakeService;
use App\Services\StatusService;
use App\Services\TrendingHashtagService;
use App\Services\UserFilterService;
use App\Status;
use Auth;
use Cache;
use DB;
use Illuminate\Http\Request;
class DiscoverController extends Controller
{
public function home(Request $request)
{
abort_if(!Auth::check() && config('instance.discover.public') == false, 403);
return view('discover.home');
}
public function showTags(Request $request, $hashtag)
{
if ($request->user()) {
return redirect('/i/web/hashtag/'.$hashtag.'?src=pd');
}
abort_if(!config('instance.discover.tags.is_public') && !Auth::check(), 403);
$tag = Hashtag::whereName($hashtag)
->orWhere('slug', $hashtag)
->where('is_banned', '!=', true)
->firstOrFail();
$tagCount = $tag->cached_count ?? 0;
$tagCount = StatusHashtagService::count($tag->id);
return view('discover.tags.show', compact('tag', 'tagCount'));
}
@ -56,7 +54,7 @@ class DiscoverController extends Controller
$this->validate($request, [
'hashtag' => 'required|string|min:1|max:124',
'page' => 'nullable|integer|min:1|max:'.($user ? 29 : 3),
'page' => 'nullable|integer|min:1|max:' . ($user ? 29 : 3)
]);
$page = $request->input('page') ?? '1';
@ -77,7 +75,7 @@ class DiscoverController extends Controller
}
$res['hashtag'] = [
'name' => $hashtag->name,
'url' => $hashtag->url(),
'url' => $hashtag->url()
];
if($user) {
$tags = StatusHashtagService::get($hashtag->id, $page, $end);
@ -86,21 +84,18 @@ class DiscoverController extends Controller
$tag['status']['favourited'] = (bool) LikeService::liked($user->profile_id, $tag['status']['id']);
$tag['status']['reblogged'] = (bool) ReblogService::get($user->profile_id, $tag['status']['id']);
$tag['status']['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $tag['status']['id']);
return $tag;
})
->filter(function($tag) {
if(!StatusService::get($tag['status']['id'])) {
return false;
}
return true;
})
->values();
} else {
if($page != 1) {
$res['tags'] = [];
return $res;
}
$key = 'discover:tags:public_feed:' . $hashtag->id . ':page:' . $page;
@ -110,7 +105,6 @@ class DiscoverController extends Controller
if(!$tag['status']['local']) {
return false;
}
return true;
})
->values();
@ -120,12 +114,10 @@ class DiscoverController extends Controller
if(!StatusService::get($tag['status']['id'])) {
return false;
}
return true;
})
->values();
}
return $res;
}
@ -152,13 +144,12 @@ class DiscoverController extends Controller
$ttls = [
1 => 1500,
31 => 14400,
365 => 86400,
365 => 86400
];
$key = ':api:discover:trending:v2.12:range:' . $days;
$ids = Cache::remember($key, $ttls[$days], function() use($days) {
$min_id = SnowflakeService::byDate(now()->subDays($days));
return DB::table('statuses')
->select(
'id',
@ -174,7 +165,7 @@ class DiscoverController extends Controller
->whereIn('type', [
'photo',
'photo:album',
'video',
'video'
])
->whereIsNsfw(false)
->orderBy('likes_count','desc')
@ -201,7 +192,6 @@ class DiscoverController extends Controller
abort_if(!$request->user(), 403);
$res = TrendingHashtagService::getTrending();
return $res;
}
@ -246,7 +236,6 @@ class DiscoverController extends Controller
->map(function($id) {
$status = StatusService::get($id, false);
$status['favourited'] = true;
return $status;
})
->filter(function($post) {
@ -288,25 +277,24 @@ class DiscoverController extends Controller
if($cc) {
return is_string($cc) ? json_decode($cc, true) : $cc;
}
return [
'hashtags' => [
'enabled' => false,
'enabled' => true,
],
'memories' => [
'enabled' => false,
'enabled' => true,
],
'insights' => [
'enabled' => false,
'enabled' => true,
],
'friends' => [
'enabled' => false,
'enabled' => true,
],
'server' => [
'enabled' => false,
'mode' => 'allowlist',
'domains' => [],
],
'domains' => []
]
];
}
@ -333,14 +321,12 @@ class DiscoverController extends Controller
return $post && isset($post['account']);
})
->values();
return $res;
}
public function enabledFeatures(Request $request)
{
abort_if(!$request->user(), 404);
return $this->config();
}
@ -369,7 +355,6 @@ class DiscoverController extends Controller
if(!Instance::whereDomain($domain)->exists()) {
return false;
}
return true;
});
$parts = array_slice($parts, 0, 10);
@ -377,47 +362,6 @@ class DiscoverController extends Controller
$res['server']['domains'] = $d;
}
ConfigCacheService::put('config.discover.features', json_encode($res));
return $res;
}
public function discoverAccountsPopular(Request $request)
{
abort_if(! $request->user(), 403);
$pid = $request->user()->profile_id;
$ids = Cache::remember('api:v1.1:discover:accounts:popular', 14400, function () {
return DB::table('profiles')
->where('is_private', false)
->whereNull('status')
->orderByDesc('profiles.followers_count')
->limit(30)
->get();
});
$filters = UserFilterService::filters($pid);
$asf = AdminShadowFilterService::getHideFromPublicFeedsList();
$ids = $ids->map(function ($profile) {
return AccountService::get($profile->id, true);
})
->filter(function ($profile) {
return $profile && isset($profile['id'], $profile['locked']) && ! $profile['locked'];
})
->filter(function ($profile) use ($pid) {
return $profile['id'] != $pid;
})
->filter(function ($profile) use ($pid) {
return ! FollowerService::follows($pid, $profile['id'], true);
})
->filter(function ($profile) use ($asf) {
return ! in_array($profile['id'], $asf);
})
->filter(function ($profile) use ($filters) {
return ! in_array($profile['id'], $filters);
})
->take(16)
->values();
return response()->json($ids, 200, [], JSON_UNESCAPED_SLASHES);
}
}

View File

@ -2,25 +2,40 @@
namespace App\Http\Controllers;
use App\Jobs\InboxPipeline\DeleteWorker;
use App\Jobs\InboxPipeline\InboxValidator;
use App\Jobs\InboxPipeline\InboxWorker;
use App\Profile;
use App\Services\AccountService;
use App\Services\InstanceService;
use App\Status;
use App\Jobs\InboxPipeline\{
DeleteWorker,
InboxWorker,
InboxValidator
};
use App\Jobs\RemoteFollowPipeline\RemoteFollowPipeline;
use App\{
AccountLog,
Like,
Profile,
Status,
User
};
use App\Util\Lexer\Nickname;
use App\Util\Site\Nodeinfo;
use App\Util\Webfinger\Webfinger;
use Auth;
use Cache;
use Carbon\Carbon;
use Illuminate\Http\Request;
use League\Fractal;
use App\Util\Site\Nodeinfo;
use App\Util\ActivityPub\{
Helpers,
HttpSignature,
Outbox
};
use Zttp\Zttp;
use App\Services\InstanceService;
class FederationController extends Controller
{
public function nodeinfoWellKnown()
{
abort_if(!config('federation.nodeinfo.enabled'), 404);
return response()->json(Nodeinfo::wellKnown(), 200, [], JSON_UNESCAPED_SLASHES)
->header('Access-Control-Allow-Origin','*');
}
@ -28,7 +43,6 @@ class FederationController extends Controller
public function nodeinfo()
{
abort_if(!config('federation.nodeinfo.enabled'), 404);
return response()->json(Nodeinfo::get(), 200, [], JSON_UNESCAPED_SLASHES)
->header('Access-Control-Allow-Origin','*');
}
@ -45,60 +59,28 @@ class FederationController extends Controller
$resource = $request->input('resource');
$domain = config('pixelfed.domain.app');
// Instance Actor
if (
config('federation.activitypub.sharedInbox') &&
$resource == 'acct:'.$domain.'@'.$domain
) {
if(config('federation.activitypub.sharedInbox') &&
$resource == 'acct:' . $domain . '@' . $domain) {
$res = [
'subject' => 'acct:' . $domain . '@' . $domain,
'aliases' => [
'https://'.$domain.'/i/actor',
'https://' . $domain . '/i/actor'
],
'links' => [
[
'rel' => 'http://webfinger.net/rel/profile-page',
'type' => 'text/html',
'href' => 'https://'.$domain.'/site/kb/instance-actor',
'href' => 'https://' . $domain . '/site/kb/instance-actor'
],
[
'rel' => 'self',
'type' => 'application/activity+json',
'href' => 'https://'.$domain.'/i/actor',
],
],
'href' => 'https://' . $domain . '/i/actor'
]
]
];
return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES);
}
if (str_starts_with($resource, 'https://')) {
if (str_starts_with($resource, 'https://'.$domain.'/users/')) {
$username = str_replace('https://'.$domain.'/users/', '', $resource);
if (strlen($username) > 15) {
return response('', 400);
}
$stripped = str_replace(['_', '.', '-'], '', $username);
if (! ctype_alnum($stripped)) {
return response('', 400);
}
$key = 'federation:webfinger:sha256:url-username:'.$username;
if ($cached = Cache::get($key)) {
return response()->json($cached, 200, [], JSON_UNESCAPED_SLASHES);
}
$profile = Profile::whereUsername($username)->first();
if (! $profile || $profile->status !== null || $profile->domain) {
return response('', 400);
}
$webfinger = (new Webfinger($profile))->generate();
Cache::put($key, $webfinger, 1209600);
return response()->json($webfinger, 200, [], JSON_UNESCAPED_SLASHES)
->header('Access-Control-Allow-Origin', '*');
} else {
return response('', 400);
}
}
$hash = hash('sha256', $resource);
$key = 'federation:webfinger:sha256:' . $hash;
if($cached = Cache::get($key)) {
@ -112,8 +94,8 @@ class FederationController extends Controller
return response('', 400);
}
$username = $parsed['username'];
$profile = Profile::whereUsername($username)->first();
if (! $profile || $profile->status !== null || $profile->domain) {
$profile = Profile::whereNull('domain')->whereUsername($username)->first();
if(!$profile || $profile->status !== null) {
return response('', 400);
}
$webfinger = (new Webfinger($profile))->generate();
@ -135,21 +117,18 @@ class FederationController extends Controller
public function userOutbox(Request $request, $username)
{
abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
abort_if(!config_cache('federation.activitypub.enabled'), 404);
if(!$request->wantsJson()) {
return redirect('/' . $username);
}
$id = AccountService::usernameToId($username);
abort_if(! $id, 404);
$account = AccountService::get($id);
abort_if(! $account || ! isset($account['statuses_count']), 404);
$res = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => 'https://' . config('pixelfed.domain.app') . '/users/' . $username . '/outbox',
'type' => 'OrderedCollection',
'totalItems' => $account['statuses_count'] ?? 0,
'totalItems' => 0,
'orderedItems' => []
];
return response(json_encode($res, JSON_UNESCAPED_SLASHES))->header('Content-Type', 'application/activity+json');
@ -157,7 +136,7 @@ class FederationController extends Controller
public function userInbox(Request $request, $username)
{
abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
abort_if(!config_cache('federation.activitypub.enabled'), 404);
abort_if(!config('federation.activitypub.inbox'), 404);
$headers = $request->headers->all();
@ -179,7 +158,6 @@ class FederationController extends Controller
if($obj['object']['type'] === 'Person') {
if(Profile::whereRemoteUrl($obj['object']['id'])->exists()) {
dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox');
return;
}
}
@ -187,30 +165,27 @@ class FederationController extends Controller
if($obj['object']['type'] === 'Tombstone') {
if(Status::whereObjectUrl($obj['object']['id'])->exists()) {
dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
return;
}
}
if($obj['object']['type'] === 'Story') {
dispatch(new DeleteWorker($headers, $payload))->onQueue('story');
return;
}
}
return;
} else if( isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) {
dispatch(new InboxValidator($username, $headers, $payload))->onQueue('follow');
} else {
dispatch(new InboxValidator($username, $headers, $payload))->onQueue('high');
}
return;
}
public function sharedInbox(Request $request)
{
abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
abort_if(!config_cache('federation.activitypub.enabled'), 404);
abort_if(!config('federation.activitypub.sharedInbox'), 404);
$headers = $request->headers->all();
@ -235,7 +210,6 @@ class FederationController extends Controller
if($obj['object']['type'] === 'Person') {
if(Profile::whereRemoteUrl($obj['object']['id'])->exists()) {
dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox');
return;
}
}
@ -243,59 +217,50 @@ class FederationController extends Controller
if($obj['object']['type'] === 'Tombstone') {
if(Status::whereObjectUrl($obj['object']['id'])->exists()) {
dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
return;
}
}
if($obj['object']['type'] === 'Story') {
dispatch(new DeleteWorker($headers, $payload))->onQueue('story');
return;
}
}
return;
} else if( isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) {
dispatch(new InboxWorker($headers, $payload))->onQueue('follow');
} else {
dispatch(new InboxWorker($headers, $payload))->onQueue('shared');
}
return;
}
public function userFollowing(Request $request, $username)
{
abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
abort_if(!config_cache('federation.activitypub.enabled'), 404);
$id = AccountService::usernameToId($username);
abort_if(! $id, 404);
$account = AccountService::get($id);
abort_if(! $account || ! isset($account['following_count']), 404);
$obj = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $request->getUri(),
'type' => 'OrderedCollection',
'totalItems' => $account['following_count'] ?? 0,
'type' => 'OrderedCollectionPage',
'totalItems' => 0,
'orderedItems' => []
];
return response()->json($obj)->header('Content-Type', 'application/activity+json');
return response()->json($obj);
}
public function userFollowers(Request $request, $username)
{
abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
$id = AccountService::usernameToId($username);
abort_if(! $id, 404);
$account = AccountService::get($id);
abort_if(! $account || ! isset($account['followers_count']), 404);
abort_if(!config_cache('federation.activitypub.enabled'), 404);
$obj = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $request->getUri(),
'type' => 'OrderedCollection',
'totalItems' => $account['followers_count'] ?? 0,
'type' => 'OrderedCollectionPage',
'totalItems' => 0,
'orderedItems' => []
];
return response()->json($obj)->header('Content-Type', 'application/activity+json');
return response()->json($obj);
}
}

View File

@ -17,7 +17,7 @@ trait Instagram
{
public function instagram()
{
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
if(config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
return view('settings.import.instagram.home');
@ -25,9 +25,6 @@ trait Instagram
public function instagramStart(Request $request)
{
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
$completed = ImportJob::whereProfileId(Auth::user()->profile->id)
->whereService('instagram')
->whereNotNull('completed_at')
@ -41,9 +38,6 @@ trait Instagram
protected function instagramRedirectOrNew()
{
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
$profile = Auth::user()->profile;
$exists = ImportJob::whereProfileId($profile->id)
->whereService('instagram')
@ -67,9 +61,6 @@ trait Instagram
public function instagramStepOne(Request $request, $uuid)
{
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
$profile = Auth::user()->profile;
$job = ImportJob::whereProfileId($profile->id)
->whereNull('completed_at')
@ -81,9 +72,6 @@ trait Instagram
public function instagramStepOneStore(Request $request, $uuid)
{
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
$max = 'max:' . config('pixelfed.import.instagram.limits.size');
$this->validate($request, [
'media.*' => 'required|mimes:bin,jpeg,png,gif|'.$max,
@ -126,9 +114,6 @@ trait Instagram
public function instagramStepTwo(Request $request, $uuid)
{
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
$profile = Auth::user()->profile;
$job = ImportJob::whereProfileId($profile->id)
->whereNull('completed_at')
@ -140,9 +125,6 @@ trait Instagram
public function instagramStepTwoStore(Request $request, $uuid)
{
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
$this->validate($request, [
'media' => 'required|file|max:1000'
]);
@ -168,9 +150,6 @@ trait Instagram
public function instagramStepThree(Request $request, $uuid)
{
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
$profile = Auth::user()->profile;
$job = ImportJob::whereProfileId($profile->id)
->whereService('instagram')
@ -183,9 +162,6 @@ trait Instagram
public function instagramStepThreeStore(Request $request, $uuid)
{
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
$profile = Auth::user()->profile;
try {

View File

@ -83,17 +83,6 @@ class ImportPostController extends Controller
);
}
public function formatHashtags($val = false)
{
if(!$val || !strlen($val)) {
return null;
}
$groupedHashtagRegex = '/#\w+(?=#)/';
return preg_replace($groupedHashtagRegex, '$0 ', $val);
}
public function store(Request $request)
{
abort_unless(config('import.instagram.enabled'), 404);
@ -139,11 +128,11 @@ class ImportPostController extends Controller
$ip->media = $c->map(function($m) {
return [
'uri' => $m['uri'],
'title' => $this->formatHashtags($m['title']),
'title' => $m['title'],
'creation_timestamp' => $m['creation_timestamp']
];
})->toArray();
$ip->caption = $c->count() > 1 ? $this->formatHashtags($file['title']) : $this->formatHashtags($ip->media[0]['title']);
$ip->caption = $c->count() > 1 ? $file['title'] : $ip->media[0]['title'];
$ip->filename = last(explode('/', $ip->media[0]['uri']));
$ip->metadata = $c->map(function($m) {
return [
@ -179,7 +168,7 @@ class ImportPostController extends Controller
'required',
'file',
$mimes,
'max:' . config_cache('pixelfed.max_photo_size')
'max:' . config('pixelfed.max_photo_size')
]
]);

View File

@ -2,9 +2,10 @@
namespace App\Http\Controllers;
use App\Http\Resources\DirectoryProfile;
use App\Profile;
use Illuminate\Http\Request;
use App\Profile;
use App\Services\AccountService;
use App\Http\Resources\DirectoryProfile;
class LandingController extends Controller
{
@ -14,7 +15,7 @@ class LandingController extends Controller
return redirect('/');
}
abort_if((bool) config_cache('instance.landing.show_directory') == false, 404);
abort_if(config_cache('instance.landing.show_directory') == false, 404);
return view('site.index');
}
@ -25,14 +26,14 @@ class LandingController extends Controller
return redirect('/');
}
abort_if((bool) config_cache('instance.landing.show_explore') == false, 404);
abort_if(config_cache('instance.landing.show_explore') == false, 404);
return view('site.index');
}
public function getDirectoryApi(Request $request)
{
abort_if((bool) config_cache('instance.landing.show_directory') == false, 404);
abort_if(config_cache('instance.landing.show_directory') == false, 404);
return DirectoryProfile::collection(
Profile::whereNull('domain')

View File

@ -25,7 +25,8 @@ class LikeController extends Controller
'item' => 'required|integer|min:1',
]);
abort(422, 'Deprecated API Endpoint');
// API deprecated
return;
$user = Auth::user();
$profile = $user->profile;
@ -33,7 +34,7 @@ class LikeController extends Controller
if (Like::whereStatusId($status->id)->whereProfileId($profile->id)->exists()) {
$like = Like::whereProfileId($profile->id)->whereStatusId($status->id)->firstOrFail();
UnlikePipeline::dispatch($like)->onQueue('feed');
UnlikePipeline::dispatch($like);
} else {
abort_if(
Like::whereProfileId($user->profile_id)
@ -59,7 +60,7 @@ class LikeController extends Controller
]) == false;
$like->save();
$status->save();
LikePipeline::dispatch($like)->onQueue('feed');
LikePipeline::dispatch($like);
}
}

View File

@ -2,15 +2,14 @@
namespace App\Http\Controllers;
use App\Media;
use Illuminate\Http\Request;
use App\Media;
class MediaController extends Controller
{
public function index(Request $request)
{
//return view('settings.drive.index');
abort(404);
}
public function composeUpdate(Request $request, $id)
@ -20,7 +19,7 @@ class MediaController extends Controller
public function fallbackRedirect(Request $request, $pid, $mhash, $uhash, $f)
{
abort_if(! (bool) config_cache('pixelfed.cloud_storage'), 404);
abort_if(!config_cache('pixelfed.cloud_storage'), 404);
$path = 'public/m/_v2/' . $pid . '/' . $mhash . '/' . $uhash . '/' . $f;
$media = Media::whereProfileId($pid)
->whereMediaPath($path)

View File

@ -1,231 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\ParentalControls;
use App\Models\UserRoles;
use App\Profile;
use App\User;
use App\Http\Controllers\Auth\RegisterController;
use Illuminate\Auth\Events\Registered;
use Illuminate\Support\Facades\Auth;
use App\Services\UserRoleService;
use App\Jobs\ParentalControlsPipeline\DispatchChildInvitePipeline;
class ParentalControlsController extends Controller
{
public function authPreflight($request, $maxUserCheck = false, $authCheck = true)
{
if($authCheck) {
abort_unless($request->user(), 404);
abort_unless($request->user()->has_roles === 0, 404);
}
abort_unless(config('instance.parental_controls.enabled'), 404);
if(config_cache('pixelfed.open_registration') == false) {
abort_if(config('instance.parental_controls.limits.respect_open_registration'), 404);
}
if($maxUserCheck == true) {
$hasLimit = config('pixelfed.enforce_max_users');
if($hasLimit) {
$count = User::where(function($q){ return $q->whereNull('status')->orWhereNotIn('status', ['deleted','delete']); })->count();
$limit = (int) config('pixelfed.max_users');
abort_if($limit && $limit <= $count, 404);
}
}
}
public function index(Request $request)
{
$this->authPreflight($request);
$children = ParentalControls::whereParentId($request->user()->id)->latest()->paginate(5);
return view('settings.parental-controls.index', compact('children'));
}
public function add(Request $request)
{
$this->authPreflight($request, true);
return view('settings.parental-controls.add');
}
public function view(Request $request, $id)
{
$this->authPreflight($request);
$uid = $request->user()->id;
$pc = ParentalControls::whereParentId($uid)->findOrFail($id);
return view('settings.parental-controls.manage', compact('pc'));
}
public function update(Request $request, $id)
{
$this->authPreflight($request);
$uid = $request->user()->id;
$ff = $this->requestFormFields($request);
$pc = ParentalControls::whereParentId($uid)->findOrFail($id);
$pc->permissions = $ff;
$pc->save();
$roles = UserRoleService::mapActions($pc->child_id, $ff);
if(isset($roles['account-force-private'])) {
$c = Profile::whereUserId($pc->child_id)->first();
$c->is_private = $roles['account-force-private'];
$c->save();
}
UserRoles::whereUserId($pc->child_id)->update(['roles' => $roles]);
return redirect($pc->manageUrl() . '?permissions');
}
public function store(Request $request)
{
$this->authPreflight($request, true);
$this->validate($request, [
'email' => 'required|email|unique:parental_controls,email|unique:users,email',
]);
$state = $this->requestFormFields($request);
$pc = new ParentalControls;
$pc->parent_id = $request->user()->id;
$pc->email = $request->input('email');
$pc->verify_code = str_random(32);
$pc->permissions = $state;
$pc->save();
DispatchChildInvitePipeline::dispatch($pc);
return redirect($pc->manageUrl());
}
public function inviteRegister(Request $request, $id, $code)
{
if($request->user()) {
$title = 'You cannot complete this action on this device.';
$body = 'Please log out or use a different device or browser to complete the invitation registration.';
return view('errors.custom', compact('title', 'body'));
}
$this->authPreflight($request, true, false);
$pc = ParentalControls::whereRaw('verify_code = BINARY ?', $code)->whereNull(['email_verified_at', 'child_id'])->findOrFail($id);
abort_unless(User::whereId($pc->parent_id)->exists(), 404);
return view('settings.parental-controls.invite-register-form', compact('pc'));
}
public function inviteRegisterStore(Request $request, $id, $code)
{
if($request->user()) {
$title = 'You cannot complete this action on this device.';
$body = 'Please log out or use a different device or browser to complete the invitation registration.';
return view('errors.custom', compact('title', 'body'));
}
$this->authPreflight($request, true, false);
$pc = ParentalControls::whereRaw('verify_code = BINARY ?', $code)->whereNull('email_verified_at')->findOrFail($id);
$fields = $request->all();
$fields['email'] = $pc->email;
$defaults = UserRoleService::defaultRoles();
$validator = (new RegisterController)->validator($fields);
$valid = $validator->validate();
abort_if(!$valid, 404);
event(new Registered($user = (new RegisterController)->create($fields)));
sleep(5);
$user->has_roles = true;
$user->parent_id = $pc->parent_id;
if(config('instance.parental_controls.limits.auto_verify_email')) {
$user->email_verified_at = now();
$user->save();
sleep(3);
} else {
$user->save();
sleep(3);
}
$ur = UserRoles::updateOrCreate([
'user_id' => $user->id,
],[
'roles' => UserRoleService::mapInvite($user->id, $pc->permissions)
]);
$pc->email_verified_at = now();
$pc->child_id = $user->id;
$pc->save();
sleep(2);
Auth::guard()->login($user);
return redirect('/i/web');
}
public function cancelInvite(Request $request, $id)
{
$this->authPreflight($request);
$pc = ParentalControls::whereParentId($request->user()->id)
->whereNull(['email_verified_at', 'child_id'])
->findOrFail($id);
return view('settings.parental-controls.delete-invite', compact('pc'));
}
public function cancelInviteHandle(Request $request, $id)
{
$this->authPreflight($request);
$pc = ParentalControls::whereParentId($request->user()->id)
->whereNull(['email_verified_at', 'child_id'])
->findOrFail($id);
$pc->delete();
return redirect('/settings/parental-controls');
}
public function stopManaging(Request $request, $id)
{
$this->authPreflight($request);
$pc = ParentalControls::whereParentId($request->user()->id)
->whereNotNull(['email_verified_at', 'child_id'])
->findOrFail($id);
return view('settings.parental-controls.stop-managing', compact('pc'));
}
public function stopManagingHandle(Request $request, $id)
{
$this->authPreflight($request);
$pc = ParentalControls::whereParentId($request->user()->id)
->whereNotNull(['email_verified_at', 'child_id'])
->findOrFail($id);
$pc->child()->update([
'has_roles' => false,
'parent_id' => null,
]);
$pc->delete();
return redirect('/settings/parental-controls');
}
protected function requestFormFields($request)
{
$state = [];
$fields = [
'post',
'comment',
'like',
'share',
'follow',
'bookmark',
'story',
'collection',
'discovery_feeds',
'dms',
'federation',
'hide_network',
'private',
'hide_cw'
];
foreach ($fields as $field) {
$state[$field] = $request->input($field) == 'on';
}
return $state;
}
}

View File

@ -2,15 +2,12 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\ConfigCache;
use Storage;
use App\Services\AccountService;
use App\Services\StatusService;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Cache;
use Storage;
use App\Status;
use App\User;
class PixelfedDirectoryController extends Controller
{
@ -29,7 +26,6 @@ class PixelfedDirectoryController extends Controller
}
$res = $this->buildListing();
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
@ -71,7 +67,7 @@ class PixelfedDirectoryController extends Controller
'display_name' => $post['account']['display_name'],
'username' => $post['account']['username'],
'media' => $post['media_attachments'][0]['url'],
'url' => $post['url'],
'url' => $post['url']
];
})
->values();
@ -82,11 +78,10 @@ class PixelfedDirectoryController extends Controller
$res['community_guidelines'] = json_decode($guidelines->v, true);
}
$openRegistration = (bool) config_cache('pixelfed.open_registration');
$res['open_registration'] = $openRegistration;
$curatedOnboarding = (bool) config_cache('instance.curated_registration.enabled');
$res['curated_onboarding'] = $curatedOnboarding;
$openRegistration = ConfigCache::whereK('pixelfed.open_registration')->first();
if($openRegistration) {
$res['open_registration'] = (bool) $openRegistration;
}
$oauthEnabled = ConfigCache::whereK('pixelfed.oauth_enabled')->first();
if($oauthEnabled) {
@ -102,14 +97,14 @@ class PixelfedDirectoryController extends Controller
$res['feature_config'] = [
'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','),
'image_quality' => config_cache('pixelfed.image_quality'),
'optimize_image' => (bool) config_cache('pixelfed.optimize_image'),
'optimize_image' => config_cache('pixelfed.optimize_image'),
'max_photo_size' => config_cache('pixelfed.max_photo_size'),
'max_caption_length' => config_cache('pixelfed.max_caption_length'),
'max_altext_length' => config_cache('pixelfed.max_altext_length'),
'enforce_account_limit' => (bool) config_cache('pixelfed.enforce_account_limit'),
'enforce_account_limit' => config_cache('pixelfed.enforce_account_limit'),
'max_account_size' => config_cache('pixelfed.max_account_size'),
'max_album_length' => config_cache('pixelfed.max_album_length'),
'account_deletion' => (bool) config_cache('pixelfed.account_deletion'),
'account_deletion' => config_cache('pixelfed.account_deletion'),
];
$res['is_eligible'] = $this->validVal($res, 'admin') &&
@ -123,32 +118,25 @@ class PixelfedDirectoryController extends Controller
$res['testimonials'] = collect(json_decode(config_cache('pixelfed.directory.testimonials'), true))
->map(function($testimonial) {
$profile = AccountService::get($testimonial['profile_id']);
return [
'profile' => [
'username' => $profile['username'],
'display_name' => $profile['display_name'],
'avatar' => $profile['avatar'],
'created_at' => $profile['created_at'],
'created_at' => $profile['created_at']
],
'body' => $testimonial['body'],
'body' => $testimonial['body']
];
});
}
$res['features_enabled'] = [
'stories' => (bool) config_cache('instance.stories.enabled'),
'stories' => (bool) config_cache('instance.stories.enabled')
];
$statusesCount = Cache::remember('api:nodeinfo:statuses', 21600, function() {
return Status::whereLocal(true)->count();
});
$usersCount = Cache::remember('api:nodeinfo:users', 43200, function() {
return User::count();
});
$res['stats'] = [
'user_count' => (int) $usersCount,
'post_count' => (int) $statusesCount,
'user_count' => \App\User::count(),
'post_count' => \App\Status::whereNull('uri')->count(),
];
$res['primary_locale'] = config('app.locale');
@ -175,4 +163,5 @@ class PixelfedDirectoryController extends Controller
return $res[$val];
}
}

View File

@ -1,93 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Models\ProfileAlias;
use App\Models\ProfileMigration;
use App\Services\AccountService;
use App\Services\WebfingerService;
use App\Util\Lexer\Nickname;
use Cache;
use Illuminate\Http\Request;
class ProfileAliasController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function index(Request $request)
{
$aliases = $request->user()->profile->aliases;
return view('settings.aliases.index', compact('aliases'));
}
public function store(Request $request)
{
$this->validate($request, [
'acct' => 'required',
]);
$acct = $request->input('acct');
$nn = Nickname::normalizeProfileUrl($acct);
if (! $nn) {
return back()->with('error', 'Invalid account alias.');
}
if ($nn['domain'] === config('pixelfed.domain.app')) {
if (strtolower($nn['username']) == ($request->user()->username)) {
return back()->with('error', 'You cannot add an alias to your own account.');
}
}
if ($request->user()->profile->aliases->count() >= 3) {
return back()->with('error', 'You can only add 3 account aliases.');
}
$webfingerService = WebfingerService::lookup($acct);
$webfingerUrl = WebfingerService::rawGet($acct);
if (! $webfingerService || ! isset($webfingerService['url']) || ! $webfingerUrl || empty($webfingerUrl)) {
return back()->with('error', 'Invalid account, cannot add alias at this time.');
}
$alias = new ProfileAlias;
$alias->profile_id = $request->user()->profile_id;
$alias->acct = $acct;
$alias->uri = $webfingerUrl;
$alias->save();
Cache::forget('pf:activitypub:user-object:by-id:'.$request->user()->profile_id);
return back()->with('status', 'Successfully added alias!');
}
public function delete(Request $request)
{
$this->validate($request, [
'acct' => 'required',
'id' => 'required|exists:profile_aliases',
]);
$pid = $request->user()->profile_id;
$acct = $request->input('acct');
$alias = ProfileAlias::where('profile_id', $pid)
->where('acct', $acct)
->findOrFail($request->input('id'));
$migration = ProfileMigration::whereProfileId($pid)
->whereAcct($acct)
->first();
if ($migration) {
$request->user()->profile->update([
'moved_to_profile_id' => null,
]);
}
$alias->delete();
Cache::forget('pf:activitypub:user-object:by-id:'.$pid);
AccountService::del($pid);
return back()->with('status', 'Successfully deleted alias!');
}
}

View File

@ -2,36 +2,33 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Auth;
use Cache;
use DB;
use View;
use App\AccountInterstitial;
use App\Follower;
use App\FollowRequest;
use App\Profile;
use App\Story;
use App\Status;
use App\User;
use App\UserSetting;
use App\UserFilter;
use League\Fractal;
use App\Services\AccountService;
use App\Services\FollowerService;
use App\Services\StatusService;
use App\Status;
use App\Story;
use App\Util\Lexer\Nickname;
use App\Util\Webfinger\Webfinger;
use App\Transformer\ActivityPub\ProfileOutbox;
use App\Transformer\ActivityPub\ProfileTransformer;
use App\User;
use App\UserFilter;
use App\UserSetting;
use Auth;
use Cache;
use Illuminate\Http\Request;
use League\Fractal;
use View;
class ProfileController extends Controller
{
public function show(Request $request, $username)
{
if ($request->wantsJson() && (bool) config_cache('federation.activitypub.enabled')) {
$user = $this->getCachedUser($username, true);
abort_if(! $user, 404, 'Not found');
return $this->showActivityPub($request, $user);
}
// redirect authed users to Metro 2.0
if($request->user()) {
// unless they force static view
@ -43,11 +40,17 @@ class ProfileController extends Controller
}
}
$user = $this->getCachedUser($username);
$user = Profile::whereNull('domain')
->whereNull('status')
->whereUsername($username)
->firstOrFail();
abort_unless($user, 404);
$aiCheck = Cache::remember('profile:ai-check:spam-login:'.$user->id, 3600, function () use ($user) {
if($request->wantsJson() && config_cache('federation.activitypub.enabled')) {
return $this->showActivityPub($request, $user);
}
$aiCheck = Cache::remember('profile:ai-check:spam-login:' . $user->id, 86400, function() use($user) {
$exists = AccountInterstitial::whereUserId($user->user_id)->where('is_spam', 1)->count();
if($exists) {
return true;
@ -58,7 +61,6 @@ class ProfileController extends Controller
if($aiCheck) {
return redirect('/login');
}
return $this->buildProfile($request, $user);
}
@ -77,7 +79,6 @@ class ProfileController extends Controller
if ($user->is_private == true) {
$profile = null;
return view('profile.private', compact('user'));
}
@ -89,14 +90,13 @@ class ProfileController extends Controller
'crawlable' => $settings->crawlable,
'following' => [
'count' => $settings->show_profile_following_count,
'list' => $settings->show_profile_following,
'list' => $settings->show_profile_following
],
'followers' => [
'count' => $settings->show_profile_follower_count,
'list' => $settings->show_profile_followers,
],
'list' => $settings->show_profile_followers
]
];
return view('profile.show', compact('profile', 'settings'));
} else {
$key = 'profile:settings:' . $user->id;
@ -118,7 +118,6 @@ class ProfileController extends Controller
$requested = Auth::check() ? FollowRequest::whereFollowerId(Auth::user()->profile_id)
->whereFollowingId($user->id)
->exists() : false;
return view('profile.private', compact('user', 'is_following', 'requested'));
}
@ -128,52 +127,25 @@ class ProfileController extends Controller
'crawlable' => $settings->crawlable,
'following' => [
'count' => $settings->show_profile_following_count,
'list' => $settings->show_profile_following,
'list' => $settings->show_profile_following
],
'followers' => [
'count' => $settings->show_profile_follower_count,
'list' => $settings->show_profile_followers,
],
'list' => $settings->show_profile_followers
]
];
return view('profile.show', compact('profile', 'settings'));
}
}
protected function getCachedUser($username, $withTrashed = false)
{
$val = str_replace(['_', '.', '-'], '', $username);
if (! ctype_alnum($val)) {
return;
}
$hash = ($withTrashed ? 'wt:' : 'wot:').strtolower($username);
return Cache::remember('pfc:cached-user:'.$hash, ($withTrashed ? 14400 : 900), function () use ($username, $withTrashed) {
if (! $withTrashed) {
return Profile::whereNull(['domain', 'status'])
->whereUsername($username)
->first();
} else {
return Profile::withTrashed()
->whereNull('domain')
->whereUsername($username)
->first();
}
});
}
public function permalinkRedirect(Request $request, $username)
{
if ($request->wantsJson() && (bool) config_cache('federation.activitypub.enabled')) {
$user = $this->getCachedUser($username, true);
$user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
if ($request->wantsJson() && config_cache('federation.activitypub.enabled')) {
return $this->showActivityPub($request, $user);
}
$user = $this->getCachedUser($username);
abort_if(! $user, 404);
return redirect($user->url());
}
@ -208,7 +180,6 @@ class ProfileController extends Controller
default:
break;
}
return abort(404);
}
@ -230,14 +201,12 @@ class ProfileController extends Controller
public function showActivityPub(Request $request, $user)
{
abort_if(!config_cache('federation.activitypub.enabled'), 404);
abort_if(! $user, 404, 'Not found');
abort_if($user->domain, 404);
return Cache::remember('pf:activitypub:user-object:by-id:'.$user->id, 1800, function () use ($user) {
return Cache::remember('pf:activitypub:user-object:by-id:' . $user->id, 3600, function() use($user) {
$fractal = new Fractal\Manager();
$resource = new Fractal\Resource\Item($user, new ProfileTransformer);
$res = $fractal->createData($resource)->toArray();
return response(json_encode($res['data']))->header('Content-Type', 'application/activity+json');
});
}
@ -254,7 +223,7 @@ class ProfileController extends Controller
abort_if(!$profile || $profile['locked'] || !$profile['local'], 404);
$aiCheck = Cache::remember('profile:ai-check:spam-login:'.$profile['id'], 3600, function () use ($profile) {
$aiCheck = Cache::remember('profile:ai-check:spam-login:' . $profile['id'], 86400, function() use($profile) {
$uid = User::whereProfileId($profile['id'])->first();
if(!$uid) {
return true;
@ -269,7 +238,7 @@ class ProfileController extends Controller
abort_if($aiCheck, 404);
$enabled = Cache::remember('profile:atom:enabled:'.$profile['id'], 86400, function () use ($profile) {
$enabled = Cache::remember('profile:atom:enabled:' . $profile['id'], 84600, function() use ($profile) {
$uid = User::whereProfileId($profile['id'])->first();
if(!$uid) {
return false;
@ -284,7 +253,7 @@ class ProfileController extends Controller
abort_if(!$enabled, 404);
$data = Cache::remember('pf:atom:user-feed:by-id:'.$profile['id'], 14400, function () use ($pid, $profile) {
$data = Cache::remember('pf:atom:user-feed:by-id:' . $profile['id'], 900, function() use($pid, $profile) {
$items = Status::whereProfileId($pid)
->whereScope('public')
->whereIn('type', ['photo', 'photo:album'])
@ -311,13 +280,12 @@ class ProfileController extends Controller
return compact('items', 'permalink', 'headers');
});
abort_if(!$data || !isset($data['items']) || !isset($data['permalink']), 404);
return response()
->view('atom.user',
[
'profile' => $profile,
'items' => $data['items'],
'permalink' => $data['permalink'],
'permalink' => $data['permalink']
]
)
->withHeaders($data['headers']);
@ -326,7 +294,6 @@ class ProfileController extends Controller
public function meRedirect()
{
abort_if(!Auth::check(), 404);
return redirect(Auth::user()->url());
}
@ -334,7 +301,7 @@ class ProfileController extends Controller
{
$res = view('profile.embed-removed');
if (! (bool) config_cache('instance.embed.profile')) {
if(!config('instance.embed.profile')) {
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
@ -342,13 +309,17 @@ class ProfileController extends Controller
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
$profile = $this->getCachedUser($username);
$profile = Profile::whereUsername($username)
->whereIsPrivate(false)
->whereNull('status')
->whereNull('domain')
->first();
if (! $profile || $profile->is_private) {
if(!$profile) {
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
$aiCheck = Cache::remember('profile:ai-check:spam-login:'.$profile->id, 3600, function () use ($profile) {
$aiCheck = Cache::remember('profile:ai-check:spam-login:' . $profile->id, 86400, function() use($profile) {
$exists = AccountInterstitial::whereUserId($profile->user_id)->where('is_spam', 1)->count();
if($exists) {
return true;
@ -367,13 +338,12 @@ class ProfileController extends Controller
$profile = AccountService::get($profile->id);
$res = view('profile.embed', compact('profile'));
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
public function stories(Request $request, $username)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
$pid = $profile->id;
$authed = Auth::user()->profile_id;
@ -382,7 +352,6 @@ class ProfileController extends Controller
->whereActive(true)
->exists();
abort_unless($exists, 404);
return view('profile.story', compact('pid', 'profile'));
}
}

View File

@ -1,72 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\ProfileMigrationStoreRequest;
use App\Jobs\ProfilePipeline\ProfileMigrationDeliverMoveActivityPipeline;
use App\Jobs\ProfilePipeline\ProfileMigrationMoveFollowersPipeline;
use App\Models\ProfileAlias;
use App\Models\ProfileMigration;
use App\Services\AccountService;
use App\Services\WebfingerService;
use App\Util\ActivityPub\Helpers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Bus;
class ProfileMigrationController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function index(Request $request)
{
abort_if((bool) config_cache('federation.activitypub.enabled') === false, 404);
if ((bool) config_cache('federation.migration') === false) {
return redirect(route('help.account-migration'));
}
$hasExistingMigration = ProfileMigration::whereProfileId($request->user()->profile_id)
->where('created_at', '>', now()->subDays(30))
->exists();
return view('settings.migration.index', compact('hasExistingMigration'));
}
public function store(ProfileMigrationStoreRequest $request)
{
abort_if((bool) config_cache('federation.activitypub.enabled') === false, 404);
$acct = WebfingerService::rawGet($request->safe()->acct);
if (! $acct) {
return redirect()->back()->withErrors(['acct' => 'The new account you provided is not responding to our requests.']);
}
$newAccount = Helpers::profileFetch($acct);
if (! $newAccount) {
return redirect()->back()->withErrors(['acct' => 'An error occured, please try again later. Code: res-failed-account-fetch']);
}
$user = $request->user();
ProfileAlias::updateOrCreate([
'profile_id' => $user->profile_id,
'acct' => $request->safe()->acct,
'uri' => $acct,
]);
$migration = ProfileMigration::create([
'profile_id' => $request->user()->profile_id,
'acct' => $request->safe()->acct,
'followers_count' => $request->user()->profile->followers_count,
'target_profile_id' => $newAccount['id'],
]);
$user->profile->update([
'moved_to_profile_id' => $newAccount->id,
'indexable' => false,
]);
AccountService::del($user->profile_id);
Bus::batch([
new ProfileMigrationDeliverMoveActivityPipeline($migration, $user->profile, $newAccount),
new ProfileMigrationMoveFollowersPipeline($user->profile_id, $newAccount->id),
])->onQueue('follow')->dispatch();
return redirect()->back()->with(['status' => 'Succesfully migrated account!']);
}
}

View File

@ -42,7 +42,6 @@ use App\Services\{
use App\Jobs\StatusPipeline\NewStatusPipeline;
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use App\Services\InstanceService;
class PublicApiController extends Controller
{
@ -662,10 +661,6 @@ class PublicApiController extends Controller
public function account(Request $request, $id)
{
$res = AccountService::get($id);
if($res && isset($res['local'], $res['url']) && !$res['local']) {
$domain = parse_url($res['url'], PHP_URL_HOST);
abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
}
return response()->json($res);
}
@ -685,11 +680,6 @@ class PublicApiController extends Controller
$profile = AccountService::get($id);
abort_if(!$profile, 404);
if($profile && isset($profile['local'], $profile['url']) && !$profile['local']) {
$domain = parse_url($profile['url'], PHP_URL_HOST);
abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
}
$limit = $request->limit ?? 9;
$max_id = $request->max_id;
$min_id = $request->min_id;

View File

@ -2,36 +2,31 @@
namespace App\Http\Controllers;
use App\Models\RemoteAuth;
use App\Services\Account\RemoteAuthService;
use App\Services\EmailService;
use App\Services\MediaStorageService;
use App\User;
use App\Util\ActivityPub\Helpers;
use App\Util\Lexer\RestrictedNames;
use Illuminate\Auth\Events\Registered;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use App\Services\Account\RemoteAuthService;
use App\Models\RemoteAuth;
use App\Profile;
use App\Instance;
use App\User;
use Purify;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Illuminate\Auth\Events\Registered;
use App\Util\Lexer\RestrictedNames;
use App\Services\EmailService;
use App\Services\MediaStorageService;
use App\Util\ActivityPub\Helpers;
use InvalidArgumentException;
use Purify;
class RemoteAuthController extends Controller
{
public function start(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
abort_unless(config_cache('pixelfed.open_registration') && config('remote-auth.mastodon.enabled'), 404);
if($request->user()) {
return redirect('/');
}
return view('auth.remote.start');
}
@ -42,13 +37,7 @@ class RemoteAuthController extends Controller
public function getAuthDomains(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
abort_unless(config_cache('pixelfed.open_registration') && config('remote-auth.mastodon.enabled'), 404);
if(config('remote-auth.mastodon.domains.only_custom')) {
$res = config('remote-auth.mastodon.domains.custom');
@ -56,7 +45,6 @@ class RemoteAuthController extends Controller
return [];
}
$res = explode(',', $res);
return response()->json($res);
}
@ -70,7 +58,6 @@ class RemoteAuthController extends Controller
return [];
}
$res = explode(',', $res);
return response()->json($res);
}
@ -82,14 +69,7 @@ class RemoteAuthController extends Controller
public function redirect(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
abort_unless(config_cache('pixelfed.open_registration') && config('remote-auth.mastodon.enabled'), 404);
$this->validate($request, ['domain' => 'required']);
$domain = $request->input('domain');
@ -98,9 +78,8 @@ class RemoteAuthController extends Controller
$res = [
'domain' => $domain,
'ready' => false,
'action' => 'incompatible_domain',
'action' => 'incompatible_domain'
];
return response()->json($res);
}
@ -110,9 +89,8 @@ class RemoteAuthController extends Controller
$res = [
'domain' => $domain,
'ready' => false,
'action' => 'blocked_domain',
'action' => 'blocked_domain'
];
return response()->json($res);
}
@ -122,9 +100,8 @@ class RemoteAuthController extends Controller
$res = [
'domain' => $domain,
'ready' => false,
'action' => 'incompatible_domain',
'action' => 'incompatible_domain'
];
return response()->json($res);
}
@ -134,9 +111,8 @@ class RemoteAuthController extends Controller
$res = [
'domain' => $domain,
'ready' => false,
'action' => 'incompatible_domain',
'action' => 'incompatible_domain'
];
return response()->json($res);
}
}
@ -147,9 +123,8 @@ class RemoteAuthController extends Controller
$res = [
'domain' => $domain,
'ready' => false,
'action' => 'incompatible_domain',
'action' => 'incompatible_domain'
];
return response()->json($res);
}
}
@ -175,7 +150,7 @@ class RemoteAuthController extends Controller
$res = [
'domain' => $domain,
'ready' => true,
'dsh' => $dsh,
'dsh' => $dsh
];
return response()->json($res);
@ -183,14 +158,6 @@ class RemoteAuthController extends Controller
public function preflight(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
if(!$request->filled('d') || !$request->filled('dsh') || !$request->session()->exists('oauth_redirect_to')) {
return redirect('/login');
}
@ -200,14 +167,6 @@ class RemoteAuthController extends Controller
public function handleCallback(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
$domain = $request->session()->get('oauth_domain');
if($request->filled('code')) {
@ -224,12 +183,10 @@ class RemoteAuthController extends Controller
if(!$res || !isset($res['access_token'])) {
$request->session()->regenerate();
return redirect('/login');
}
$request->session()->put('oauth_remote_session_token', $res['access_token']);
return redirect('/auth/mastodon/getting-started');
}
@ -238,29 +195,15 @@ class RemoteAuthController extends Controller
public function onboarding(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
abort_unless(config_cache('pixelfed.open_registration') && config('remote-auth.mastodon.enabled'), 404);
if($request->user()) {
return redirect('/');
}
return view('auth.remote.onboarding');
}
public function sessionCheck(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
abort_if($request->user(), 403);
abort_unless($request->session()->exists('oauth_domain'), 403);
abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
@ -282,7 +225,7 @@ class RemoteAuthController extends Controller
return response()->json([
'code' => 200,
'msg' => 'Success!',
'action' => 'max_uses_reached',
'action' => 'max_uses_reached'
]);
}
}
@ -292,26 +235,19 @@ class RemoteAuthController extends Controller
return response()->json([
'code' => 200,
'msg' => 'Success!',
'action' => 'redirect_existing_user',
'action' => 'redirect_existing_user'
]);
}
return response()->json([
'code' => 200,
'msg' => 'Success!',
'action' => 'onboard',
'action' => 'onboard'
]);
}
public function sessionGetMastodonData(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
abort_if($request->user(), 403);
abort_unless($request->session()->exists('oauth_domain'), 403);
abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
@ -333,7 +269,7 @@ class RemoteAuthController extends Controller
'bearer_token' => $token,
'verify_credentials' => $res,
'last_verify_credentials_at' => now(),
'last_successful_login_at' => now(),
'last_successful_login_at' => now()
]);
$request->session()->put('oauth_masto_raid', $ra->id);
@ -343,13 +279,6 @@ class RemoteAuthController extends Controller
public function sessionValidateUsername(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
abort_if($request->user(), 403);
abort_unless($request->session()->exists('oauth_domain'), 403);
abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
@ -389,8 +318,8 @@ class RemoteAuthController extends Controller
if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
return $fail('Username cannot be used.');
}
},
],
}
]
]);
$username = strtolower($request->input('username'));
@ -399,19 +328,12 @@ class RemoteAuthController extends Controller
return response()->json([
'code' => 200,
'username' => $username,
'exists' => $exists,
'exists' => $exists
]);
}
public function sessionValidateEmail(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
abort_if($request->user(), 403);
abort_unless($request->session()->exists('oauth_domain'), 403);
abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
@ -420,7 +342,7 @@ class RemoteAuthController extends Controller
'email' => [
'required',
'email:strict,filter_unicode,dns,spoof',
],
]
]);
$email = $request->input('email');
@ -431,19 +353,12 @@ class RemoteAuthController extends Controller
'code' => 200,
'email' => $email,
'exists' => $exists,
'banned' => $banned,
'banned' => $banned
]);
}
public function sessionGetMastodonFollowers(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
abort_unless($request->session()->exists('oauth_domain'), 403);
abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
abort_unless($request->session()->exists('oauth_remasto_id'), 403);
@ -457,7 +372,7 @@ class RemoteAuthController extends Controller
if(!$res) {
return response()->json([
'code' => 200,
'following' => [],
'following' => []
]);
}
@ -465,19 +380,12 @@ class RemoteAuthController extends Controller
return response()->json([
'code' => 200,
'following' => $res,
'following' => $res
]);
}
public function handleSubmit(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
abort_unless($request->session()->exists('oauth_domain'), 403);
abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
abort_unless($request->session()->exists('oauth_remasto_id'), 403);
@ -521,10 +429,10 @@ class RemoteAuthController extends Controller
if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
return $fail('Username cannot be used.');
}
},
}
],
'password' => 'required|string|min:8|confirmed',
'name' => 'nullable|max:30',
'name' => 'nullable|max:30'
]);
$email = $request->input('email');
@ -536,7 +444,7 @@ class RemoteAuthController extends Controller
'name' => $name,
'username' => $username,
'password' => $password,
'email' => $email,
'email' => $email
]);
$raid = $request->session()->pull('oauth_masto_raid');
@ -550,19 +458,13 @@ class RemoteAuthController extends Controller
return [
'code' => 200,
'msg' => 'Success',
'token' => $token,
'token' => $token
];
}
public function storeBio(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
abort_unless(config_cache('pixelfed.open_registration') && config('remote-auth.mastodon.enabled'), 404);
abort_unless($request->user(), 404);
abort_unless($request->session()->exists('oauth_domain'), 403);
abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
@ -581,20 +483,14 @@ class RemoteAuthController extends Controller
public function accountToId(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
abort_unless(config_cache('pixelfed.open_registration') && config('remote-auth.mastodon.enabled'), 404);
abort_if($request->user(), 404);
abort_unless($request->session()->exists('oauth_domain'), 403);
abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
abort_unless($request->session()->exists('oauth_remasto_id'), 403);
$this->validate($request, [
'account' => 'required|url',
'account' => 'required|url'
]);
$account = $request->input('account');
@ -629,13 +525,7 @@ class RemoteAuthController extends Controller
public function storeAvatar(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
abort_unless(config_cache('pixelfed.open_registration') && config('remote-auth.mastodon.enabled'), 404);
abort_unless($request->user(), 404);
$this->validate($request, [
'avatar_url' => 'required|active_url',
@ -650,20 +540,14 @@ class RemoteAuthController extends Controller
$avatar->remote_url = $request->input('avatar_url');
$avatar->save();
MediaStorageService::avatar($avatar, (bool) config_cache('pixelfed.cloud_storage') == false);
MediaStorageService::avatar($avatar, config_cache('pixelfed.cloud_storage') == false);
return [200];
}
public function finishUp(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
abort_unless(config_cache('pixelfed.open_registration') && config('remote-auth.mastodon.enabled'), 404);
abort_unless($request->user(), 404);
$currentWebfinger = '@' . $request->user()->username . '@' . config('pixelfed.domain.app');
@ -680,13 +564,7 @@ class RemoteAuthController extends Controller
public function handleLogin(Request $request)
{
abort_unless((
config_cache('pixelfed.open_registration') &&
config('remote-auth.mastodon.enabled')
) || (
config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled')
), 404);
abort_unless(config_cache('pixelfed.open_registration') && config('remote-auth.mastodon.enabled'), 404);
abort_if($request->user(), 404);
abort_unless($request->session()->exists('oauth_domain'), 403);
abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
@ -700,7 +578,6 @@ class RemoteAuthController extends Controller
$user = User::findOrFail($ra->user_id);
abort_if($user->is_admin || $user->status != null, 422, 'Invalid auth action');
Auth::loginUsingId($ra->user_id);
return [200];
}
@ -713,7 +590,7 @@ class RemoteAuthController extends Controller
'password' => Hash::make($data['password']),
'email_verified_at' => config('remote-auth.mastodon.contraints.skip_email_verification') ? now() : null,
'app_register_ip' => request()->ip(),
'register_source' => 'mastodon',
'register_source' => 'mastodon'
])));
$this->guarder()->login($user);

View File

@ -2,25 +2,27 @@
namespace App\Http\Controllers;
use Auth;
use App\Hashtag;
use App\Place;
use App\Profile;
use App\Services\WebfingerService;
use App\Status;
use App\Util\ActivityPub\Helpers;
use Auth;
use Illuminate\Http\Request;
use App\Util\ActivityPub\Helpers;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use App\Transformer\Api\{
AccountTransformer,
HashtagTransformer,
StatusTransformer,
};
use App\Services\WebfingerService;
class SearchController extends Controller
{
public $tokens = [];
public $term = '';
public $hash = '';
public $cacheKey = 'api:search:tag:';
public function __construct()
@ -34,7 +36,7 @@ class SearchController extends Controller
'q' => 'required|string|min:3|max:120',
'src' => 'required|string|in:metro',
'v' => 'required|integer|in:2',
'scope' => 'required|in:all,hashtag,profile,remote,webfinger',
'scope' => 'required|in:all,hashtag,profile,remote,webfinger'
]);
$scope = $request->input('scope') ?? 'all';
@ -82,13 +84,12 @@ class SearchController extends Controller
$hash = hash('sha256', $tag);
if( Helpers::validateUrl($tag) != false &&
Helpers::validateLocalUrl($tag) != true &&
(bool) config_cache('federation.activitypub.enabled') == true &&
config_cache('federation.activitypub.enabled') == true &&
config('federation.activitypub.remoteFollow') == true
) {
$remote = Helpers::fetchFromUrl($tag);
if( isset($remote['type']) &&
in_array($remote['type'], ['Note', 'Question'])
) {
$remote['type'] == 'Note') {
$item = Helpers::statusFetch($tag);
$this->tokens['posts'] = [[
'count' => 0,
@ -121,7 +122,7 @@ class SearchController extends Controller
'tokens' => [$item->caption],
'name' => $item->caption,
'thumb' => $item->thumb(),
'filter' => $item->firstMedia()->filter_class,
'filter' => $item->firstMedia()->filter_class
];
});
$this->tokens['posts'] = $posts;
@ -152,7 +153,6 @@ class SearchController extends Controller
'name' => null,
];
});
return $tags;
}
});
@ -180,7 +180,7 @@ class SearchController extends Controller
'tokens' => '',
'name' => null,
'city' => $item->name,
'country' => $item->country,
'country' => $item->country
];
});
// return $tags;
@ -190,7 +190,7 @@ class SearchController extends Controller
$this->tokens['placesPagination'] = [
'total' => $hashtags->total(),
'current_page' => $hashtags->currentPage(),
'last_page' => $hashtags->lastPage(),
'last_page' => $hashtags->lastPage()
];
}
@ -203,7 +203,7 @@ class SearchController extends Controller
$ttl = now()->addHours(2);
if( Helpers::validateUrl($tag) != false &&
Helpers::validateLocalUrl($tag) != true &&
(bool) config_cache('federation.activitypub.enabled') == true &&
config_cache('federation.activitypub.enabled') == true &&
config('federation.activitypub.remoteFollow') == true
) {
$remote = Helpers::fetchFromUrl($tag);
@ -225,14 +225,15 @@ class SearchController extends Controller
'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
'thumb' => $item->avatarUrl(),
'local' => (bool) !$item->domain,
'post_count' => $item->statuses()->count(),
],
'post_count' => $item->statuses()->count()
]
]];
return $tokens;
});
}
} else {
}
else {
$this->tokens['profiles'] = Cache::remember($key, $ttl, function() use($tag) {
if(Str::startsWith($tag, '@')) {
$tag = substr($tag, 1);
@ -261,8 +262,8 @@ class SearchController extends Controller
'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
'thumb' => $item->avatarUrl(),
'local' => (bool) !$item->domain,
'post_count' => $item->statuses()->count(),
],
'post_count' => $item->statuses()->count()
]
];
});
}
@ -300,11 +301,11 @@ class SearchController extends Controller
'following' => null,
'follow_request' => null,
'thumb' => $wfs['avatar'],
'local' => (bool) $wfs['local'],
],
],
'local' => (bool) $wfs['local']
]
]
];
return;
}
protected function remotePostLookup()
@ -332,7 +333,7 @@ class SearchController extends Controller
'username' => $item->profile->username,
'caption' => $item->rendered ?? $item->caption,
'thumb' => $url,
'timestamp' => $item->created_at->diffForHumans(),
'timestamp' => $item->created_at->diffForHumans()
]];
}
@ -352,7 +353,7 @@ class SearchController extends Controller
'username' => $item->profile->username,
'caption' => $item->rendered ?? $item->caption,
'thumb' => $url,
'timestamp' => $item->created_at->diffForHumans(),
'timestamp' => $item->created_at->diffForHumans()
]];
}
}

View File

@ -139,8 +139,8 @@ trait HomeSettings
$log->action = 'account.edit.password';
$log->message = 'Password changed';
$log->link = null;
$log->ip_address = $request->ip();
$log->user_agent = $request->userAgent();
$log->ip_address = "127.0.0.23";
$log->user_agent = "Pixelfed.de";
$log->save();
Mail::to($request->user())->send(new PasswordChange($user));
@ -173,8 +173,7 @@ trait HomeSettings
$user->email = $email;
if ($validate) {
// auto verify admin email addresses
$user->email_verified_at = $user->is_admin == true ? now() : null;
$user->email_verified_at = null;
// Prevent old verifications from working
EmailVerification::whereUserId($user->id)->delete();
}
@ -186,8 +185,8 @@ trait HomeSettings
$log->action = 'account.edit.email';
$log->message = 'Email changed';
$log->link = null;
$log->ip_address = $request->ip();
$log->user_agent = $request->userAgent();
$log->ip_address = "127.0.0.23";
$log->user_agent = "Pixelfed.de";
$log->save();
}
@ -196,7 +195,7 @@ trait HomeSettings
$user->save();
$profile->save();
return redirect('/settings/email')->with('status', 'Email successfully updated!');
return redirect('/settings/home')->with('status', 'Email successfully updated!');
} else {
return redirect('/settings/email');
}
@ -207,4 +206,5 @@ trait HomeSettings
{
return view('settings.avatar');
}
}

View File

@ -2,17 +2,22 @@
namespace App\Http\Controllers\Settings;
use App\AccountLog;
use App\EmailVerification;
use App\Instance;
use App\Follower;
use App\Media;
use App\Profile;
use App\Services\RelationshipService;
use App\User;
use App\UserFilter;
use Auth;
use Cache;
use DB;
use App\Util\Lexer\PrettyNumber;
use App\Util\ActivityPub\Helpers;
use Auth, Cache, DB;
use Illuminate\Http\Request;
trait PrivacySettings
{
public function privacy()
{
$user = Auth::user();
@ -34,11 +39,9 @@ trait PrivacySettings
'public_dm',
'show_profile_follower_count',
'show_profile_following_count',
'indexable',
'show_atom',
];
$profile->indexable = $request->input('indexable') == 'on';
$profile->is_suggestable = $request->input('is_suggestable') == 'on';
$profile->save();
@ -67,8 +70,6 @@ trait PrivacySettings
} else {
$settings->{$field} = false;
}
} elseif ($field == 'indexable') {
} else {
if ($form == 'on') {
$settings->{$field} = true;
@ -78,20 +79,14 @@ trait PrivacySettings
}
$settings->save();
}
$pid = $profile->id;
Cache::forget('profile:settings:'.$pid);
Cache::forget('profile:settings:' . $profile->id);
Cache::forget('user:account:id:' . $profile->user_id);
Cache::forget('profile:follower_count:'.$pid);
Cache::forget('profile:following_count:'.$pid);
Cache::forget('profile:atom:enabled:'.$pid);
Cache::forget('profile:embed:'.$pid);
Cache::forget('pf:acct:settings:hidden-followers:'.$pid);
Cache::forget('pf:acct:settings:hidden-following:'.$pid);
Cache::forget('pf:acct-trans:hideFollowing:'.$pid);
Cache::forget('pf:acct-trans:hideFollowers:'.$pid);
Cache::forget('pfc:cached-user:wt:'.strtolower($profile->username));
Cache::forget('pfc:cached-user:wot:'.strtolower($profile->username));
Cache::forget('profile:follower_count:' . $profile->id);
Cache::forget('profile:following_count:' . $profile->id);
Cache::forget('profile:atom:enabled:' . $profile->id);
Cache::forget('profile:embed:' . $profile->id);
Cache::forget('pf:acct:settings:hidden-followers:' . $profile->id);
Cache::forget('pf:acct:settings:hidden-following:' . $profile->id);
return redirect(route('settings.privacy'))->with('status', 'Settings successfully updated!');
}
@ -100,14 +95,13 @@ trait PrivacySettings
$pid = Auth::user()->profile->id;
$ids = (new UserFilter())->mutedUserIds($pid);
$users = Profile::whereIn('id', $ids)->simplePaginate(15);
return view('settings.privacy.muted', compact('users'));
}
public function mutedUsersUpdate(Request $request)
{
$this->validate($request, [
'profile_id' => 'required|integer|min:1',
'profile_id' => 'required|integer|min:1'
]);
$fid = $request->input('profile_id');
$pid = Auth::user()->profile->id;
@ -119,8 +113,6 @@ trait PrivacySettings
->firstOrFail();
$filter->delete();
});
RelationshipService::refresh($pid, $fid);
return redirect()->back();
}
@ -129,14 +121,14 @@ trait PrivacySettings
$pid = Auth::user()->profile->id;
$ids = (new UserFilter())->blockedUserIds($pid);
$users = Profile::whereIn('id', $ids)->simplePaginate(15);
return view('settings.privacy.blocked', compact('users'));
}
public function blockedUsersUpdate(Request $request)
{
$this->validate($request, [
'profile_id' => 'required|integer|min:1',
'profile_id' => 'required|integer|min:1'
]);
$fid = $request->input('profile_id');
$pid = Auth::user()->profile->id;
@ -148,32 +140,52 @@ trait PrivacySettings
->firstOrFail();
$filter->delete();
});
RelationshipService::refresh($pid, $fid);
return redirect()->back();
}
public function blockedInstances()
{
// deprecated
abort(404);
}
public function domainBlocks()
{
return view('settings.privacy.domain-blocks');
$pid = Auth::user()->profile->id;
$filters = UserFilter::whereUserId($pid)
->whereFilterableType('App\Instance')
->whereFilterType('block')
->orderByDesc('id')
->paginate(10);
return view('settings.privacy.blocked-instances', compact('filters'));
}
public function blockedInstanceStore(Request $request)
{
// deprecated
abort(404);
$this->validate($request, [
'domain' => 'required|url|min:1|max:120'
]);
$domain = $request->input('domain');
if(Helpers::validateUrl($domain) == false) {
return abort(400, 'Invalid domain');
}
$domain = parse_url($domain, PHP_URL_HOST);
$instance = Instance::firstOrCreate(['domain' => $domain]);
$filter = new UserFilter;
$filter->user_id = Auth::user()->profile->id;
$filter->filterable_id = $instance->id;
$filter->filterable_type = 'App\Instance';
$filter->filter_type = 'block';
$filter->save();
return response()->json(['msg' => 200]);
}
public function blockedInstanceUnblock(Request $request)
{
// deprecated
abort(404);
$this->validate($request, [
'id' => 'required|integer|min:1'
]);
$pid = Auth::user()->profile->id;
$filter = UserFilter::whereFilterableType('App\Instance')
->whereUserId($pid)
->findOrFail($request->input('id'));
$filter->delete();
return redirect(route('settings.privacy.blocked-instances'));
}
public function blockedKeywords()
@ -211,7 +223,7 @@ trait PrivacySettings
break;
default:
// code...
# code...
break;
}
}
@ -221,7 +233,6 @@ trait PrivacySettings
$settings->save();
$profile->save();
Cache::forget('profiles:private');
return [200];
}
}

View File

@ -2,18 +2,14 @@
namespace App\Http\Controllers;
use App\Page;
use App\Profile;
use App\Services\FollowerService;
use App\Status;
use App\User;
use App\Util\ActivityPub\Helpers;
use App\Util\Localization\Localization;
use Auth;
use Cache;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use View;
use App, Auth, Cache, View;
use App\Util\Lexer\PrettyNumber;
use App\{Follower, Page, Profile, Status, User, UserFilter};
use App\Util\Localization\Localization;
use App\Services\FollowerService;
use App\Util\ActivityPub\Helpers;
class SiteController extends Controller
{
@ -62,7 +58,6 @@ class SiteController extends Controller
$user_count = number_format(User::count());
$post_count = number_format(Status::count());
$rules = config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : null;
return view('site.about', compact('rules', 'user_count', 'post_count'))->render();
});
}
@ -77,7 +72,6 @@ class SiteController extends Controller
return Cache::remember('site:help:community-guidelines', now()->addDays(120), function() {
$slug = '/site/kb/community-guidelines';
$page = Page::whereSlug($slug)->whereActive(true)->first();
return View::make('site.help.community-guidelines')->with(compact('page'))->render();
});
}
@ -86,10 +80,8 @@ class SiteController extends Controller
{
$page = Cache::remember('site:privacy', now()->addDays(120), function() {
$slug = '/site/privacy';
return Page::whereSlug($slug)->whereActive(true)->first();
});
return View::make('site.privacy')->with(compact('page'))->render();
}
@ -97,10 +89,8 @@ class SiteController extends Controller
{
$page = Cache::remember('site:terms', now()->addDays(120), function() {
$slug = '/site/terms';
return Page::whereSlug($slug)->whereActive(true)->first();
});
return View::make('site.terms')->with(compact('page'))->render();
}
@ -108,11 +98,10 @@ class SiteController extends Controller
{
abort_if(!$request->user(), 404);
$this->validate($request, [
'url' => 'required|url',
'url' => 'required|url'
]);
$url = request()->input('url');
abort_if(Helpers::validateUrl($url) == false, 404);
return view('site.redirect', compact('url'));
}
@ -125,7 +114,6 @@ class SiteController extends Controller
$user = $request->user();
abort_if($user && $profile->id == $user->profile_id, 404);
$following = $user != null ? FollowerService::follows($user->profile_id, $profile->id) : false;
return view('site.intents.follow', compact('profile', 'user', 'following'));
}
@ -171,33 +159,9 @@ class SiteController extends Controller
{
$page = Cache::remember('site:legal-notice', now()->addDays(120), function() {
$slug = '/site/legal-notice';
return Page::whereSlug($slug)->whereActive(true)->first();
});
abort_if(!$page, 404);
return View::make('site.legal-notice')->with(compact('page'))->render();
}
public function curatedOnboarding(Request $request)
{
if ($request->user()) {
return redirect('/i/web');
}
$regOpen = (bool) config_cache('pixelfed.open_registration');
$curOnboarding = (bool) config_cache('instance.curated_registration.enabled');
$curOnlyClosed = (bool) config('instance.curated_registration.state.only_enabled_on_closed_reg');
if ($regOpen) {
if ($curOnlyClosed) {
return redirect('/register');
}
} else {
if (! $curOnboarding) {
return redirect('/');
}
}
return view('auth.curated-register.index', ['step' => 1]);
}
}

View File

@ -1,21 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Services\Internal\SoftwareUpdateService;
class SoftwareUpdateController extends Controller
{
public function __construct()
{
$this->middleware('auth');
$this->middleware('admin');
}
public function getSoftwareUpdateCheck(Request $request)
{
$res = SoftwareUpdateService::get();
return $res;
}
}

View File

@ -2,26 +2,30 @@
namespace App\Http\Controllers;
use App\AccountInterstitial;
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
use App\Jobs\StatusPipeline\NewStatusPipeline;
use App\Jobs\StatusPipeline\StatusDelete;
use App\Jobs\SharePipeline\SharePipeline;
use App\Jobs\SharePipeline\UndoSharePipeline;
use App\Jobs\StatusPipeline\RemoteStatusDelete;
use App\Jobs\StatusPipeline\StatusDelete;
use App\AccountInterstitial;
use App\Media;
use App\Profile;
use App\Services\AccountService;
use App\Services\HashidService;
use App\Services\ReblogService;
use App\Services\StatusService;
use App\Status;
use App\StatusArchived;
use App\StatusView;
use App\Transformer\ActivityPub\StatusTransformer;
use App\Transformer\ActivityPub\Verb\Note;
use App\Transformer\ActivityPub\Verb\Question;
use App\Util\Media\License;
use Auth;
use Cache;
use DB;
use App\User;
use Auth, DB, Cache;
use Illuminate\Http\Request;
use League\Fractal;
use App\Util\Media\Filter;
use Illuminate\Support\Str;
use App\Services\HashidService;
use App\Services\StatusService;
use App\Util\Media\License;
use App\Services\ReblogService;
class StatusController extends Controller
{
@ -51,7 +55,6 @@ class StatusController extends Controller
if(ends_with($url, '/activity')) {
$url = str_replace('/activity', '', $url);
}
return redirect($url);
}
@ -75,25 +78,21 @@ class StatusController extends Controller
StatusView::firstOrCreate([
'status_id' => $status->id,
'status_profile_id' => $status->profile_id,
'profile_id' => $request->user()->profile_id,
'profile_id' => $request->user()->profile_id
]);
}
if ($request->wantsJson() && (bool) config_cache('federation.activitypub.enabled')) {
if ($request->wantsJson() && config_cache('federation.activitypub.enabled')) {
return $this->showActivityPub($request, $status);
}
$template = $status->in_reply_to_id ? 'status.reply' : 'status.show';
return view($template, compact('user', 'status'));
}
public function shortcodeRedirect(Request $request, $id)
{
$hid = HashidService::decode($id);
abort_if(! $hid, 404);
return redirect('/i/web/post/'.$hid);
abort(404);
}
public function showId(int $id)
@ -102,45 +101,28 @@ class StatusController extends Controller
$status = Status::whereNull('reblog_of_id')
->whereIn('scope', ['public', 'unlisted'])
->findOrFail($id);
return redirect($status->url());
}
public function showEmbed(Request $request, $username, int $id)
{
if (! (bool) config_cache('instance.embed.post')) {
if(!config('instance.embed.post')) {
$res = view('status.embed-removed');
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
$status = StatusService::get($id);
$profile = Profile::whereNull(['domain','status'])
->whereIsPrivate(false)
->whereUsername($username)
->first();
if (
! $status ||
! isset($status['account'], $status['account']['id'], $status['local']) ||
! $status['local'] ||
strtolower($status['account']['username']) !== strtolower($username)
) {
if(!$profile) {
$content = view('status.embed-removed');
return response($content, 404)->header('X-Frame-Options', 'ALLOWALL');
}
$profile = AccountService::get($status['account']['id'], true);
if (! $profile || $profile['locked'] || ! $profile['local']) {
$content = view('status.embed-removed');
return response($content)->header('X-Frame-Options', 'ALLOWALL');
}
$aiCheck = Cache::remember('profile:ai-check:spam-login:'.$profile['id'], 3600, function () use ($profile) {
$user = Profile::find($profile['id']);
if (! $user) {
return true;
}
$exists = AccountInterstitial::whereUserId($user->user_id)->where('is_spam', 1)->count();
$aiCheck = Cache::remember('profile:ai-check:spam-login:' . $profile->id, 86400, function() use($profile) {
$exists = AccountInterstitial::whereUserId($profile->user_id)->where('is_spam', 1)->count();
if($exists) {
return true;
}
@ -150,30 +132,22 @@ class StatusController extends Controller
if($aiCheck) {
$res = view('status.embed-removed');
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
$status = StatusService::get($id);
if (
! $status ||
! isset($status['account'], $status['account']['id']) ||
intval($status['account']['id']) !== intval($profile['id']) ||
$status['sensitive'] ||
$status['visibility'] !== 'public' ||
$status['pf_type'] !== 'photo'
) {
$status = Status::whereProfileId($profile->id)
->whereNull('uri')
->whereScope('public')
->whereIsNsfw(false)
->whereIn('type', ['photo', 'video','photo:album'])
->find($id);
if(!$status) {
$content = view('status.embed-removed');
return response($content)->header('X-Frame-Options', 'ALLOWALL');
}
$showLikes = $request->filled('likes') && $request->likes == true;
$showCaption = $request->filled('caption') && $request->caption !== false;
$layout = $request->filled('layout') && $request->layout == 'compact' ? 'compact' : 'full';
$content = view('status.embed', compact('status', 'showLikes', 'showCaption', 'layout'));
return response($content)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
}
@ -213,7 +187,7 @@ class StatusController extends Controller
public function store(Request $request)
{
return;
}
public function delete(Request $request)
@ -268,7 +242,7 @@ class StatusController extends Controller
Cache::forget('profile:embed:' . $status->profile_id);
StatusService::del($status->id, true);
Cache::forget('profile:status_count:'.$status->profile_id);
$status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status);
StatusDelete::dispatch($status);
}
} else if ($status->profile_id == $user->profile_id || $user->is_admin == true) {
Cache::forget('_api:statuses:recent_9:' . $status->profile_id);
@ -276,7 +250,7 @@ class StatusController extends Controller
Cache::forget('profile:embed:' . $status->profile_id);
StatusService::del($status->id, true);
Cache::forget('profile:status_count:'.$status->profile_id);
$status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status);
StatusDelete::dispatch($status);
}
if($request->wantsJson()) {
@ -355,7 +329,6 @@ class StatusController extends Controller
->with(['media'])
->findOrFail($id);
$licenses = License::get();
return view('status.edit', compact('user', 'status', 'licenses'));
}
@ -392,7 +365,6 @@ class StatusController extends Controller
protected function validateVisibility($visibility)
{
$allowed = ['public', 'unlisted', 'private'];
return in_array($visibility, $allowed) ? $visibility : 'public';
}
@ -432,12 +404,11 @@ class StatusController extends Controller
return 'text';
}
public function toggleVisibility(Request $request)
{
public function toggleVisibility(Request $request) {
$this->authCheck();
$this->validate($request, [
'item' => 'required|string|min:1|max:20',
'disableComments' => 'required|boolean',
'disableComments' => 'required|boolean'
]);
$user = Auth::user();
@ -477,7 +448,7 @@ class StatusController extends Controller
StatusView::firstOrCreate([
'status_id' => $view['sid'],
'status_profile_id' => $view['pid'],
'profile_id' => $uid,
'profile_id' => $uid
]);
});
}

View File

@ -2,63 +2,45 @@
namespace App\Http\Controllers\Stories;
use App\DirectMessage;
use App\Http\Controllers\Controller;
use App\Http\Resources\StoryView as StoryViewResource;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
use App\Models\Conversation;
use App\DirectMessage;
use App\Notification;
use App\Story;
use App\Status;
use App\StoryView;
use App\Jobs\StoryPipeline\StoryDelete;
use App\Jobs\StoryPipeline\StoryFanout;
use App\Jobs\StoryPipeline\StoryReplyDeliver;
use App\Jobs\StoryPipeline\StoryViewDeliver;
use App\Models\Conversation;
use App\Notification;
use App\Services\AccountService;
use App\Services\MediaPathService;
use App\Services\StoryService;
use App\Status;
use App\Story;
use App\StoryView;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class StoryApiV1Controller extends Controller
{
const RECENT_KEY = 'pf:stories:recent-by-id:';
const RECENT_TTL = 300;
public function carousel(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$pid = $request->user()->profile_id;
if(config('database.default') == 'pgsql') {
$s = Cache::remember(self::RECENT_KEY.$pid, self::RECENT_TTL, function () use ($pid) {
return Story::select('stories.*', 'followers.following_id')
$s = Story::select('stories.*', 'followers.following_id')
->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
->where('followers.profile_id', $pid)
->where('stories.active', true)
->map(function ($s) {
$r = new \StdClass;
$r->id = $s->id;
$r->profile_id = $s->profile_id;
$r->type = $s->type;
$r->path = $s->path;
return $r;
})
->unique('profile_id');
});
->get();
} else {
$s = Cache::remember(self::RECENT_KEY.$pid, self::RECENT_TTL, function () use ($pid) {
return Story::select('stories.*', 'followers.following_id')
$s = Story::select('stories.*', 'followers.following_id')
->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
->where('followers.profile_id', $pid)
->where('stories.active', true)
->orderBy('id')
->get();
});
}
$nodes = $s->map(function($s) use($pid) {
@ -74,7 +56,7 @@ class StoryApiV1Controller extends Controller
'src' => url(Storage::url($s->path)),
'duration' => $s->duration ?? 3,
'seen' => StoryService::hasSeen($pid, $s->id),
'created_at' => $s->created_at->format('c'),
'created_at' => $s->created_at->format('c')
];
})
->filter()
@ -83,7 +65,6 @@ class StoryApiV1Controller extends Controller
$profile = AccountService::get($item[0]['pid'], true);
$url = $profile['local'] ? url("/stories/{$profile['username']}") :
url("/i/rs/{$profile['id']}");
return [
'id' => 'pfs:' . $profile['id'],
'user' => [
@ -92,7 +73,7 @@ class StoryApiV1Controller extends Controller
'username_acct' => $profile['acct'],
'avatar' => $profile['avatar'],
'local' => $profile['local'],
'is_author' => $profile['id'] == $pid,
'is_author' => $profile['id'] == $pid
],
'nodes' => $item,
'url' => $url,
@ -111,14 +92,14 @@ class StoryApiV1Controller extends Controller
$selfStories = Story::whereProfileId($pid)
->whereActive(true)
->get()
->map(function ($s) {
->map(function($s) use($pid) {
return [
'id' => (string) $s->id,
'type' => $s->type,
'src' => url(Storage::url($s->path)),
'duration' => $s->duration,
'seen' => true,
'created_at' => $s->created_at->format('c'),
'created_at' => $s->created_at->format('c')
];
})
->sortBy('id')
@ -130,131 +111,18 @@ class StoryApiV1Controller extends Controller
'username' => $selfProfile['acct'],
'avatar' => $selfProfile['avatar'],
'local' => $selfProfile['local'],
'is_author' => true,
'is_author' => true
],
'nodes' => $selfStories,
];
}
return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
}
public function selfCarousel(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$pid = $request->user()->profile_id;
if (config('database.default') == 'pgsql') {
$s = Cache::remember(self::RECENT_KEY.$pid, self::RECENT_TTL, function () use ($pid) {
return Story::select('stories.*', 'followers.following_id')
->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
->where('followers.profile_id', $pid)
->where('stories.active', true)
->map(function ($s) {
$r = new \StdClass;
$r->id = $s->id;
$r->profile_id = $s->profile_id;
$r->type = $s->type;
$r->path = $s->path;
return $r;
})
->unique('profile_id');
});
} else {
$s = Cache::remember(self::RECENT_KEY.$pid, self::RECENT_TTL, function () use ($pid) {
return Story::select('stories.*', 'followers.following_id')
->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
->where('followers.profile_id', $pid)
->where('stories.active', true)
->orderBy('id')
->get();
});
}
$nodes = $s->map(function ($s) use ($pid) {
$profile = AccountService::get($s->profile_id, true);
if (! $profile || ! isset($profile['id'])) {
return false;
}
return [
'id' => (string) $s->id,
'pid' => (string) $s->profile_id,
'type' => $s->type,
'src' => url(Storage::url($s->path)),
'duration' => $s->duration ?? 3,
'seen' => StoryService::hasSeen($pid, $s->id),
'created_at' => $s->created_at->format('c'),
];
})
->filter()
->groupBy('pid')
->map(function ($item) use ($pid) {
$profile = AccountService::get($item[0]['pid'], true);
$url = $profile['local'] ? url("/stories/{$profile['username']}") :
url("/i/rs/{$profile['id']}");
return [
'id' => 'pfs:'.$profile['id'],
'user' => [
'id' => (string) $profile['id'],
'username' => $profile['username'],
'username_acct' => $profile['acct'],
'avatar' => $profile['avatar'],
'local' => $profile['local'],
'is_author' => $profile['id'] == $pid,
],
'nodes' => $item,
'url' => $url,
'seen' => StoryService::hasSeen($pid, StoryService::latest($profile['id'])),
];
})
->sortBy('seen')
->values();
$selfProfile = AccountService::get($pid, true);
$res = [
'self' => [
'user' => [
'id' => (string) $selfProfile['id'],
'username' => $selfProfile['acct'],
'avatar' => $selfProfile['avatar'],
'local' => $selfProfile['local'],
'is_author' => true,
],
'nodes' => [],
],
'nodes' => $nodes,
];
if (Story::whereProfileId($pid)->whereActive(true)->exists()) {
$selfStories = Story::whereProfileId($pid)
->whereActive(true)
->get()
->map(function ($s) {
return [
'id' => (string) $s->id,
'type' => $s->type,
'src' => url(Storage::url($s->path)),
'duration' => $s->duration,
'seen' => true,
'created_at' => $s->created_at->format('c'),
];
})
->sortBy('id')
->values();
$res['self']['nodes'] = $selfStories;
}
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
public function add(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'file' => function() {
@ -264,7 +132,7 @@ class StoryApiV1Controller extends Controller
'max:' . config_cache('pixelfed.max_photo_size'),
];
},
'duration' => 'sometimes|integer|min:0|max:30',
'duration' => 'sometimes|integer|min:0|max:30'
]);
$user = $request->user();
@ -300,7 +168,7 @@ class StoryApiV1Controller extends Controller
'msg' => 'Successfully added',
'media_id' => (string) $story->id,
'media_url' => url(Storage::url($url)) . '?v=' . time(),
'media_type' => $story->type,
'media_type' => $story->type
];
return $res;
@ -308,13 +176,13 @@ class StoryApiV1Controller extends Controller
public function publish(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'media_id' => 'required',
'duration' => 'required|integer|min:0|max:30',
'can_reply' => 'required|boolean',
'can_react' => 'required|boolean',
'can_react' => 'required|boolean'
]);
$id = $request->input('media_id');
@ -340,7 +208,7 @@ class StoryApiV1Controller extends Controller
public function delete(Request $request, $id)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$user = $request->user();
@ -353,13 +221,13 @@ class StoryApiV1Controller extends Controller
return [
'code' => 200,
'msg' => 'Successfully deleted',
'msg' => 'Successfully deleted'
];
}
public function viewed(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'id' => 'required|min:1',
@ -383,7 +251,7 @@ class StoryApiV1Controller extends Controller
$v = StoryView::firstOrCreate([
'story_id' => $id,
'profile_id' => $authed->id,
'profile_id' => $authed->id
]);
if($v->wasRecentlyCreated) {
@ -396,16 +264,15 @@ class StoryApiV1Controller extends Controller
Cache::forget('stories:recent:by_id:' . $authed->id);
StoryService::addSeen($authed->id, $story->id);
return ['code' => 200];
}
public function comment(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'sid' => 'required',
'caption' => 'required|string',
'caption' => 'required|string'
]);
$pid = $request->user()->profile_id;
$text = $request->input('caption');
@ -423,7 +290,7 @@ class StoryApiV1Controller extends Controller
$status->visibility = 'direct';
$status->in_reply_to_profile_id = $story->profile_id;
$status->entities = json_encode([
'story_id' => $story->id,
'story_id' => $story->id
]);
$status->save();
@ -437,20 +304,20 @@ class StoryApiV1Controller extends Controller
'story_actor_username' => $request->user()->username,
'story_id' => $story->id,
'story_media_url' => url(Storage::url($story->path)),
'caption' => $text,
'caption' => $text
]);
$dm->save();
Conversation::updateOrInsert(
[
'to_id' => $story->profile_id,
'from_id' => $pid,
'from_id' => $pid
],
[
'type' => 'story:comment',
'status_id' => $status->id,
'dm_id' => $dm->id,
'is_hidden' => false,
'is_hidden' => false
]
);
@ -468,7 +335,7 @@ class StoryApiV1Controller extends Controller
return [
'code' => 200,
'msg' => 'Sent!',
'msg' => 'Sent!'
];
}
@ -478,38 +345,14 @@ class StoryApiV1Controller extends Controller
if(in_array($photo->getMimeType(), [
'image/jpeg',
'image/png',
'video/mp4',
'video/mp4'
]) == false) {
abort(400, 'Invalid media type');
return;
}
$storagePath = MediaPathService::story($user->profile);
$path = $photo->storePubliclyAs($storagePath, Str::random(random_int(2, 12)) . '_' . Str::random(random_int(32, 35)) . '_' . Str::random(random_int(1, 14)) . '.' . $photo->extension());
return $path;
}
public function viewers(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$this->validate($request, [
'sid' => 'required|string|min:1|max:50',
]);
$pid = $request->user()->profile_id;
$sid = $request->input('sid');
$story = Story::whereProfileId($pid)
->whereActive(true)
->findOrFail($sid);
$viewers = StoryView::whereStoryId($story->id)
->orderByDesc('id')
->cursorPaginate(10);
return StoryViewResource::collection($viewers);
}
}

View File

@ -2,33 +2,39 @@
namespace App\Http\Controllers;
use App\DirectMessage;
use App\Jobs\StoryPipeline\StoryDelete;
use App\Jobs\StoryPipeline\StoryFanout;
use App\Jobs\StoryPipeline\StoryReactionDeliver;
use App\Jobs\StoryPipeline\StoryReplyDeliver;
use App\Models\Conversation;
use App\Models\Poll;
use App\Models\PollVote;
use App\Notification;
use App\Report;
use App\Services\FollowerService;
use App\Services\MediaPathService;
use App\Services\StoryService;
use App\Services\UserRoleService;
use App\Status;
use App\Story;
use FFMpeg;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use App\Media;
use App\Profile;
use App\Report;
use App\DirectMessage;
use App\Notification;
use App\Status;
use App\Story;
use App\StoryView;
use App\Models\Poll;
use App\Models\PollVote;
use App\Services\ProfileService;
use App\Services\StoryService;
use Cache, Storage;
use Image as Intervention;
use Storage;
use App\Services\FollowerService;
use App\Services\MediaPathService;
use FFMpeg;
use FFMpeg\Coordinate\Dimension;
use FFMpeg\Format\Video\X264;
use App\Jobs\StoryPipeline\StoryReactionDeliver;
use App\Jobs\StoryPipeline\StoryReplyDeliver;
use App\Jobs\StoryPipeline\StoryFanout;
use App\Jobs\StoryPipeline\StoryDelete;
use ImageOptimizer;
use App\Models\Conversation;
class StoryComposeController extends Controller
{
public function apiV1Add(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'file' => function() {
@ -41,7 +47,7 @@ class StoryComposeController extends Controller
]);
$user = $request->user();
abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
$count = Story::whereProfileId($user->profile_id)
->whereActive(true)
->where('expires_at', '>', now())
@ -73,7 +79,7 @@ class StoryComposeController extends Controller
'msg' => 'Successfully added',
'media_id' => (string) $story->id,
'media_url' => url(Storage::url($url)) . '?v=' . time(),
'media_type' => $story->type,
'media_type' => $story->type
];
if($story->type === 'video') {
@ -83,9 +89,8 @@ class StoryComposeController extends Controller
if($duration > 500) {
Storage::delete($story->path);
$story->delete();
return response()->json([
'message' => 'Video duration cannot exceed 60 seconds',
'message' => 'Video duration cannot exceed 60 seconds'
], 422);
}
}
@ -99,10 +104,9 @@ class StoryComposeController extends Controller
if(in_array($photo->getMimeType(), [
'image/jpeg',
'image/png',
'video/mp4',
'video/mp4'
]) == false) {
abort(400, 'Invalid media type');
return;
}
@ -115,20 +119,19 @@ class StoryComposeController extends Controller
$img->save($fpath, config_cache('pixelfed.image_quality'));
$img->destroy();
}
return $path;
}
public function cropPhoto(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'media_id' => 'required|integer|min:1',
'width' => 'required',
'height' => 'required',
'x' => 'required',
'y' => 'required',
'y' => 'required'
]);
$user = $request->user();
@ -163,18 +166,17 @@ class StoryComposeController extends Controller
public function publishStory(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'media_id' => 'required',
'duration' => 'required|integer|min:3|max:120',
'can_reply' => 'required|boolean',
'can_react' => 'required|boolean',
'can_react' => 'required|boolean'
]);
$id = $request->input('media_id');
$user = $request->user();
abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
$story = Story::whereProfileId($user->profile_id)
->findOrFail($id);
@ -196,7 +198,7 @@ class StoryComposeController extends Controller
public function apiV1Delete(Request $request, $id)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$user = $request->user();
@ -209,22 +211,20 @@ class StoryComposeController extends Controller
return [
'code' => 200,
'msg' => 'Successfully deleted',
'msg' => 'Successfully deleted'
];
}
public function compose(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$user = $request->user();
abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
return view('stories.compose');
}
public function createPoll(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
abort_if(!config_cache('instance.polls.enabled'), 404);
return $request->all();
@ -232,17 +232,15 @@ class StoryComposeController extends Controller
public function publishStoryPoll(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'question' => 'required|string|min:6|max:140',
'options' => 'required|array|min:2|max:4',
'can_reply' => 'required|boolean',
'can_react' => 'required|boolean',
'can_react' => 'required|boolean'
]);
$user = $request->user();
abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
$pid = $request->user()->profile_id;
$count = Story::whereProfileId($pid)
@ -258,7 +256,7 @@ class StoryComposeController extends Controller
$story->type = 'poll';
$story->story = json_encode([
'question' => $request->input('question'),
'options' => $request->input('options'),
'options' => $request->input('options')
]);
$story->public = false;
$story->local = true;
@ -292,11 +290,11 @@ class StoryComposeController extends Controller
public function storyPollVote(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'sid' => 'required',
'ci' => 'required|integer|min:0|max:3',
'ci' => 'required|integer|min:0|max:3'
]);
$pid = $request->user()->profile_id;
@ -324,16 +322,13 @@ class StoryComposeController extends Controller
public function storeReport(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'type' => 'required|alpha_dash',
'id' => 'required|integer|min:1',
]);
$user = $request->user();
abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
$pid = $request->user()->profile_id;
$sid = $request->input('id');
$type = $request->input('type');
@ -349,7 +344,7 @@ class StoryComposeController extends Controller
'copyright',
'impersonation',
'scam',
'terrorism',
'terrorism'
];
abort_if(!in_array($type, $types), 422, 'Invalid story report type');
@ -366,7 +361,7 @@ class StoryComposeController extends Controller
) {
return response()->json(['error' => [
'code' => 409,
'message' => 'Cannot report the same story again',
'message' => 'Cannot report the same story again'
]], 409);
}
@ -385,15 +380,14 @@ class StoryComposeController extends Controller
public function react(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'sid' => 'required',
'reaction' => 'required|string',
'reaction' => 'required|string'
]);
$pid = $request->user()->profile_id;
$text = $request->input('reaction');
$user = $request->user();
abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
$story = Story::findOrFail($request->input('sid'));
abort_if(!$story->can_react, 422);
@ -409,7 +403,7 @@ class StoryComposeController extends Controller
$status->in_reply_to_profile_id = $story->profile_id;
$status->entities = json_encode([
'story_id' => $story->id,
'reaction' => $text,
'reaction' => $text
]);
$status->save();
@ -423,20 +417,20 @@ class StoryComposeController extends Controller
'story_actor_username' => $request->user()->username,
'story_id' => $story->id,
'story_media_url' => url(Storage::url($story->path)),
'reaction' => $text,
'reaction' => $text
]);
$dm->save();
Conversation::updateOrInsert(
[
'to_id' => $story->profile_id,
'from_id' => $pid,
'from_id' => $pid
],
[
'type' => 'story:react',
'status_id' => $status->id,
'dm_id' => $dm->id,
'is_hidden' => false,
'is_hidden' => false
]
);
@ -460,15 +454,14 @@ class StoryComposeController extends Controller
public function comment(Request $request)
{
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'sid' => 'required',
'caption' => 'required|string',
'caption' => 'required|string'
]);
$pid = $request->user()->profile_id;
$text = $request->input('caption');
$user = $request->user();
abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
$story = Story::findOrFail($request->input('sid'));
abort_if(!$story->can_reply, 422);
@ -482,7 +475,7 @@ class StoryComposeController extends Controller
$status->visibility = 'direct';
$status->in_reply_to_profile_id = $story->profile_id;
$status->entities = json_encode([
'story_id' => $story->id,
'story_id' => $story->id
]);
$status->save();
@ -496,20 +489,20 @@ class StoryComposeController extends Controller
'story_actor_username' => $request->user()->username,
'story_id' => $story->id,
'story_media_url' => url(Storage::url($story->path)),
'caption' => $text,
'caption' => $text
]);
$dm->save();
Conversation::updateOrInsert(
[
'to_id' => $story->profile_id,
'from_id' => $pid,
'from_id' => $pid
],
[
'type' => 'story:comment',
'status_id' => $status->id,
'dm_id' => $dm->id,
'is_hidden' => false,
'is_hidden' => false
]
);

View File

@ -28,18 +28,13 @@ use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Resource\Item;
use App\Transformer\ActivityPub\Verb\StoryVerb;
use App\Jobs\StoryPipeline\StoryViewDeliver;
use App\Services\UserRoleService;
class StoryController extends StoryComposeController
{
public function recent(Request $request)
{
abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
$user = $request->user();
if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) {
return [];
}
$pid = $user->profile_id;
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$pid = $request->user()->profile_id;
if(config('database.default') == 'pgsql') {
$s = Cache::remember('pf:stories:recent-by-id:' . $pid, 900, function() use($pid) {
@ -117,13 +112,9 @@ class StoryController extends StoryComposeController
public function profile(Request $request, $id)
{
abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$user = $request->user();
if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) {
return [];
}
$authed = $user->profile_id;
$authed = $request->user()->profile_id;
$profile = Profile::findOrFail($id);
if($authed != $profile->id && !FollowerService::follows($authed, $profile->id)) {
@ -176,17 +167,14 @@ class StoryController extends StoryComposeController
public function viewed(Request $request)
{
abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'id' => 'required|min:1',
]);
$id = $request->input('id');
$user = $request->user();
if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) {
return [];
}
$authed = $user->profile;
$authed = $request->user()->profile;
$story = Story::with('profile')
->findOrFail($id);
@ -221,11 +209,8 @@ class StoryController extends StoryComposeController
public function exists(Request $request, $id)
{
abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
$user = $request->user();
if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) {
return response()->json(false);
}
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
return response()->json(Story::whereProfileId($id)
->whereActive(true)
->exists());
@ -233,7 +218,7 @@ class StoryController extends StoryComposeController
public function iRedirect(Request $request)
{
abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$user = $request->user();
abort_if(!$user, 404);
@ -243,17 +228,12 @@ class StoryController extends StoryComposeController
public function viewers(Request $request)
{
abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'sid' => 'required|string'
]);
$user = $request->user();
if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) {
return response()->json([]);
}
$pid = $request->user()->profile_id;
$sid = $request->input('sid');
@ -274,7 +254,7 @@ class StoryController extends StoryComposeController
public function remoteStory(Request $request, $id)
{
abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$profile = Profile::findOrFail($id);
if($profile->user_id != null || $profile->domain == null) {
@ -286,7 +266,7 @@ class StoryController extends StoryComposeController
public function pollResults(Request $request)
{
abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [
'sid' => 'required|string'
@ -304,7 +284,7 @@ class StoryController extends StoryComposeController
public function getActivityObject(Request $request, $username, $id)
{
abort_if(!(bool) config_cache('instance.stories.enabled'), 404);
abort_if(!config_cache('instance.stories.enabled'), 404);
if(!$request->wantsJson()) {
return redirect('/stories/' . $username);

View File

@ -1,131 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\User;
use App\Models\UserEmailForgot;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Mail;
use App\Mail\UserEmailForgotReminder;
use Illuminate\Support\Facades\RateLimiter;
class UserEmailForgotController extends Controller
{
public function __construct()
{
$this->middleware('guest');
abort_unless(config('security.forgot-email.enabled'), 404);
}
public function index(Request $request)
{
abort_if($request->user(), 404);
return view('auth.email.forgot');
}
public function store(Request $request)
{
$rules = [
'username' => 'required|min:2|max:15|exists:users'
];
$messages = [
'username.exists' => 'This username is no longer active or does not exist!'
];
if((bool) config_cache('captcha.enabled')) {
$rules['h-captcha-response'] = 'required|captcha';
$messages['h-captcha-response.required'] = 'You need to complete the captcha!';
}
$randomDelay = random_int(500000, 2000000);
usleep($randomDelay);
$this->validate($request, $rules, $messages);
$check = self::checkLimits();
if(!$check) {
return redirect()->back()->withErrors([
'username' => 'Please try again later, we\'ve reached our quota and cannot process any more requests at this time.'
]);
}
$user = User::whereUsername($request->input('username'))
->whereNotNull('email_verified_at')
->whereNull('status')
->whereIsAdmin(false)
->first();
if(!$user) {
return redirect()->back()->withErrors([
'username' => 'Invalid username or account. It may not exist, or does not have a verified email, is an admin account or is disabled.'
]);
}
$exists = UserEmailForgot::whereUserId($user->id)
->where('email_sent_at', '>', now()->subHours(24))
->count();
if($exists) {
return redirect()->back()->withErrors([
'username' => 'An email reminder was recently sent to this account, please try again after 24 hours!'
]);
}
return $this->storeHandle($request, $user);
}
protected function storeHandle($request, $user)
{
UserEmailForgot::create([
'user_id' => $user->id,
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
'email_sent_at' => now()
]);
Mail::to($user->email)->send(new UserEmailForgotReminder($user));
self::getLimits(true);
return redirect()->back()->with(['status' => 'Successfully sent an email reminder!']);
}
public static function checkLimits()
{
$limits = self::getLimits();
if(
$limits['current']['hourly'] >= $limits['max']['hourly'] ||
$limits['current']['daily'] >= $limits['max']['daily'] ||
$limits['current']['weekly'] >= $limits['max']['weekly'] ||
$limits['current']['monthly'] >= $limits['max']['monthly']
) {
return false;
}
return true;
}
public static function getLimits($forget = false)
{
return [
'max' => config('security.forgot-email.limits.max'),
'current' => [
'hourly' => self::activeCount(60, $forget),
'daily' => self::activeCount(1440, $forget),
'weekly' => self::activeCount(10080, $forget),
'monthly' => self::activeCount(43800, $forget)
]
];
}
public static function activeCount($mins, $forget = false)
{
if($forget) {
Cache::forget('pf:auth:forgot-email:active-count:dur-' . $mins);
}
return Cache::remember('pf:auth:forgot-email:active-count:dur-' . $mins, 14200, function() use($mins) {
return UserEmailForgot::where('email_sent_at', '>', now()->subMinutes($mins))->count();
});
}
}

View File

@ -1,23 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Services\UserRoleService;
class UserRolesController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function getRoles(Request $request)
{
$this->validate($request, [
'id' => 'required'
]);
return UserRoleService::getRoles($request->user()->id);
}
}

View File

@ -14,12 +14,12 @@ class Kernel extends HttpKernel
* @var array
*/
protected $middleware = [
\Illuminate\Http\Middleware\HandleCors::class,
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrustProxies::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
];
/**

View File

@ -1,80 +0,0 @@
<?php
namespace App\Http\Requests;
use App\Models\ProfileMigration;
use App\Services\FetchCacheService;
use App\Services\WebfingerService;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Validator;
class ProfileMigrationStoreRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
if ((bool) config_cache('federation.activitypub.enabled') === false ||
(bool) config_cache('federation.migration') === false) {
return false;
}
if (! $this->user() || $this->user()->status) {
return false;
}
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'acct' => 'required|email',
'password' => 'required|current_password',
];
}
public function after(): array
{
return [
function (Validator $validator) {
$err = $this->validateNewAccount();
if ($err !== 'noerr') {
$validator->errors()->add(
'acct',
$err
);
}
},
];
}
protected function validateNewAccount()
{
if (ProfileMigration::whereProfileId($this->user()->profile_id)->where('created_at', '>', now()->subDays(30))->exists()) {
return 'Error - You have migrated your account in the past 30 days, you can only perform a migration once per 30 days.';
}
$acct = WebfingerService::rawGet($this->acct);
if (! $acct) {
return 'The new account you provided is not responding to our requests.';
}
$pr = FetchCacheService::getJson($acct);
if (! $pr || ! isset($pr['alsoKnownAs'])) {
return 'Invalid account lookup response.';
}
if (! count($pr['alsoKnownAs']) || ! is_array($pr['alsoKnownAs'])) {
return 'The new account does not contain an alias to your current account.';
}
$curAcctUrl = $this->user()->profile->permalink();
if (! in_array($curAcctUrl, $pr['alsoKnownAs'])) {
return 'The new account does not contain an alias to your current account.';
}
return 'noerr';
}
}

View File

@ -2,10 +2,10 @@
namespace App\Http\Requests\Status;
use Illuminate\Foundation\Http\FormRequest;
use App\Media;
use App\Status;
use Closure;
use Illuminate\Foundation\Http\FormRequest;
class StoreStatusEditRequest extends FormRequest
{
@ -22,17 +22,16 @@ class StoreStatusEditRequest extends FormRequest
return false;
}
$types = [
'photo',
'photo:album',
'photo:video:album',
'reply',
'text',
'video',
'video:album',
"photo",
"photo:album",
"photo:video:album",
"reply",
"text",
"video",
"video:album"
];
$scopes = ['public', 'unlisted', 'private'];
$status = Status::whereNull('reblog_of_id')->whereIn('type', $types)->whereIn('scope', $scopes)->find($this->route('id'));
return $status && $this->user()->profile_id === $status->profile_id;
}
@ -51,7 +50,7 @@ class StoreStatusEditRequest extends FormRequest
'nullable',
'required_without:status',
'array',
'max:'.(int) config_cache('pixelfed.max_album_length'),
'max:' . config('pixelfed.max_album_length'),
function (string $attribute, mixed $value, Closure $fail) {
Media::whereProfileId($this->user()->profile_id)
->where(function($query) {

View File

@ -1,30 +0,0 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use App\Services\AccountService;
class AdminProfile extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
$res = AccountService::get($this->id, true);
$res['domain'] = $this->domain;
$res['status'] = $this->status;
$res['limits'] = [
'exist' => $this->cw || $this->unlisted || $this->no_autolink,
'autocw' => (bool) $this->cw,
'unlisted' => (bool) $this->unlisted,
'no_autolink' => (bool) $this->no_autolink,
'banned' => (bool) $this->status == 'banned'
];
return $res;
}
}

View File

@ -1,49 +0,0 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use App\Instance;
use App\Services\AccountService;
use App\Services\StatusService;
class AdminRemoteReport extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
$instance = parse_url($this->uri, PHP_URL_HOST);
$statuses = [];
if($this->status_ids && count($this->status_ids)) {
foreach($this->status_ids as $sid) {
$s = StatusService::get($sid, false);
if($s && $s['in_reply_to_id'] != null) {
$parent = StatusService::get($s['in_reply_to_id'], false);
if($parent) {
$s['parent'] = $parent;
}
}
if($s) {
$statuses[] = $s;
}
}
}
$res = [
'id' => $this->id,
'instance' => $instance,
'reported' => AccountService::get($this->account_id, true),
'status_ids' => $this->status_ids,
'statuses' => $statuses,
'message' => $this->comment,
'report_meta' => $this->report_meta,
'created_at' => optional($this->created_at)->format('c'),
'action_taken_at' => optional($this->action_taken_at)->format('c'),
];
return $res;
}
}

View File

@ -1,20 +0,0 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use App\Services\AccountService;
class StoryView extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request)
{
return AccountService::get($this->profile_id, true);
}
}

View File

@ -6,21 +6,7 @@ use Illuminate\Database\Eloquent\Model;
class Instance extends Model
{
protected $casts = [
'last_crawled_at' => 'datetime',
'actors_last_synced_at' => 'datetime',
'notes' => 'array',
'nodeinfo_last_fetched' => 'datetime',
'delivery_next_after' => 'datetime',
];
protected $fillable = [
'domain',
'banned',
'auto_cw',
'unlisted',
'notes'
];
protected $fillable = ['domain', 'banned', 'auto_cw', 'unlisted', 'notes'];
public function profiles()
{

View File

@ -1,139 +0,0 @@
<?php
namespace App\Jobs\AdminPipeline;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Avatar;
use App\Follower;
use App\Instance;
use App\Media;
use App\Profile;
use App\Status;
use Cache;
use Storage;
use Purify;
use App\Services\ActivityPubFetchService;
use App\Services\AccountService;
use App\Services\MediaStorageService;
use App\Services\StatusService;
use App\Jobs\StatusPipeline\RemoteStatusDelete;
class AdminProfileActionPipeline implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $action;
protected $profile;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($profile, $action)
{
$this->profile = $profile;
$this->action = $action;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$profile = $this->profile;
$action = $this->action;
switch($action) {
case 'mark-all-cw':
return $this->markAllPostsWithContentWarnings();
break;
case 'unlist-all':
return $this->unlistAllPosts();
break;
case 'purge':
return $this->purgeAllPosts();
break;
case 'refetch':
return $this->refetchAllPosts();
break;
}
}
protected function markAllPostsWithContentWarnings()
{
$profile = $this->profile;
foreach(Status::whereProfileId($profile->id)->lazyById(10, 'id') as $status) {
if($status->scope == 'direct') {
continue;
}
$status->is_nsfw = true;
$status->save();
StatusService::del($status->id);
}
}
protected function unlistAllPosts()
{
$profile = $this->profile;
foreach(Status::whereProfileId($profile->id)->lazyById(10, 'id') as $status) {
if($status->scope != 'public') {
continue;
}
$status->scope = 'unlisted';
$status->visibility = 'unlisted';
$status->save();
StatusService::del($status->id);
}
}
protected function purgeAllPosts()
{
$profile = $this->profile;
foreach(Status::withTrashed()->whereProfileId($profile->id)->lazyById(10, 'id') as $status) {
RemoteStatusDelete::dispatch($status)->onQueue('delete');
}
}
protected function refetchAllPosts()
{
$profile = $this->profile;
$res = ActivityPubFetchService::get($profile->remote_url, false);
if(!$res) {
return;
}
$res = json_decode($res, true);
$profile->following_count = Follower::whereProfileId($profile->id)->count();
$profile->followers_count = Follower::whereFollowingId($profile->id)->count();
$profile->name = isset($res['name']) ? Purify::clean($res['name']) : $profile->username;
$profile->bio = isset($res['summary']) ? Purify::clean($res['summary']) : null;
if(isset($res['publicKey'])) {
$profile->public_key = $res['publicKey']['publicKeyPem'];
}
if(
isset($res['icon']) &&
isset(
$res['icon']['type'],
$res['icon']['mediaType'],
$res['icon']['url']) && $res['icon']['type'] == 'Image'
) {
if(in_array($res['icon']['mediaType'], ['image/jpeg', 'image/png'])) {
$profile->avatar->remote_url = $res['icon']['url'];
$profile->push();
MediaStorageService::avatar($profile->avatar);
}
}
$profile->save();
AccountService::del($profile->id);
}
}

View File

@ -2,9 +2,9 @@
namespace App\Jobs\AvatarPipeline;
use Cache;
use App\Avatar;
use App\Profile;
use Cache;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
@ -20,7 +20,6 @@ class AvatarOptimize implements ShouldQueue
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $profile;
protected $current;
/**
@ -66,7 +65,7 @@ class AvatarOptimize implements ShouldQueue
Cache::forget('avatar:' . $avatar->profile_id);
$this->deleteOldAvatar($avatar->media_path, $this->current);
if ((bool) config_cache('pixelfed.cloud_storage') && (bool) config_cache('instance.avatar.local_to_cloud')) {
if(config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud')) {
$this->uploadToCloud($avatar);
} else {
$avatar->cdn_url = null;
@ -80,7 +79,8 @@ class AvatarOptimize implements ShouldQueue
{
if ( storage_path('app/'.$new) == $current ||
Str::endsWith($current, 'avatars/default.png') ||
Str::endsWith($current, 'avatars/default.jpg')) {
Str::endsWith($current, 'avatars/default.jpg'))
{
return;
}
if (is_file($current)) {

Some files were not shown because too many files have changed in this diff Show More