Uploads quebrados com instalação de subpasta

Semelhante a Uploaded avatars and Gravatar not working with subfolder installation

Todos os uploads estão quebrados na minha instalação em subpasta. Os uploads chegam ao diretório de uploads real, mas ao renderizar as postagens, todas as imagens recebem src="".

Fazendo uma postagem…
https://i.imgur.com/ofOUY4e.png

Após postar…
https://i.imgur.com/EBmnD6e.png

Surpreendentemente, se eu mudar para um navegador diferente (agora Chrome), abrir o tópico (onde a imagem ainda está quebrada), mas clicar em editar, a imagem é renderizada na prévia de edição novamente!

https://i.imgur.com/3rQirhc.png

Isso confirma que o upload está sendo feito com sucesso para o servidor, o que verifiquei:

root@cs6991:/var/discourse# ./launcher enter app
x86_64 arch detected.
root@cs6991-app:/var/www/discourse# ls 'public/~cs6991/forum'
backups  uploads
root@cs6991-app:/var/www/discourse# ls 'public/~cs6991/forum/uploads'
default
root@cs6991-app:/var/www/discourse# ls 'public/~cs6991/forum/uploads/default/original/1X/'
08335563eac3a393e60a902d4d38cffdfa6d967d.png  3eee67e6460792667bab4f2248ad4643be4feae3.png
29e403dabcfee32379629fb6d844354193e278ba.png  42ecfcb27b534acc9f3436fa7d291c2fca106e57.png

Mas simplesmente não parece estar renderizando na página real.
O mesmo problema ocorre com outros uploads, como avatares.

Algumas informações:

Subpasta: /~cs6991/forum

app.yml

## este é o modelo de contêiner Docker Discourse autônomo e tudo-em-um
##
## Após fazer alterações neste arquivo, VOCÊ DEVE reconstruir
## /var/discourse/launcher rebuild app
##
## TENHA MUITO CUIDADO AO EDITAR!
## ARQUIVOS YAML SÃO SUPER SUPER SENSÍVEIS A ERROS DE ESPAÇAMENTO OU ALINHAMENTO!
## visite http://www.yamllint.com/ para validar este arquivo conforme necessário

templates:
  - "templates/postgres.template.yml"
  - "templates/redis.template.yml"
  - "templates/web.template.yml"
  - "templates/web.ratelimited.template.yml"
## Descomente estas duas linhas se desejar adicionar o Let's Encrypt (https)
  #- "templates/web.ssl.template.yml"
  #- "templates/web.letsencrypt.ssl.template.yml"

## quais portas TCP/IP este contêiner deve expor?
## Se você quiser que o Discourse compartilhe uma porta com outro servidor web como Apache ou nginx,
## veja https://meta.discourse.org/t/17247 para detalhes
expose:
  - "80:80"   # http
  - "443:443" # https

params:
  db_default_text_search_config: "pg_catalog.english"

  ## Defina db_shared_buffers para um máximo de 25% da memória total.
  ## será definido automaticamente pelo bootstrap com base na RAM detectada, ou você pode substituir
  db_shared_buffers: "128MB"

  ## pode melhorar o desempenho da classificação, mas aumenta o uso de memória por conexão
  #db_work_mem: "40MB"

  ## Qual revisão Git este contêiner deve usar? (padrão: tests-passed)
  #version: tests-passed

env:
  LC_ALL: en_US.UTF-8
  LANG: en_US.UTF-8
  LANGUAGE: en_US.UTF-8
  # DISCOURSE_DEFAULT_LOCALE: en

  ## Quantas requisições web concorrentes são suportadas? Depende da memória e dos núcleos da CPU.
  ## será definido automaticamente pelo bootstrap com base nas CPUs detectadas, ou você pode substituir
  UNICORN_WORKERS: 2

  ## TODO: O nome de domínio que esta instância do Discourse responderá
  ## Obrigatório. O Discourse não funcionará com um número IP simples.
  DISCOURSE_HOSTNAME: 'cgi.cse.unsw.edu.au'

  ## Descomente se você quiser que o contêiner seja iniciado com o mesmo
  ## nome de host (-h option) que o especificado acima (padrão "$hostname-$config")
  #DOCKER_USE_HOSTNAME: true

  ## TODO: Lista de e-mails separados por vírgula que serão administradores e desenvolvedores
  ## na inscrição inicial, exemplo 'user1@example.com,user2@example.com'
  DISCOURSE_DEVELOPER_EMAILS: '<<REDACTED>>'

  ## TODO: O servidor de e-mail SMTP usado para validar novas contas e enviar notificações
  # ENDEREÇO SMTP, nome de usuário e senha são obrigatórios
  # AVISO o caractere '#' na senha SMTP pode causar problemas!
  DISCOURSE_SMTP_ADDRESS: email-smtp.ap-southeast-2.amazonaws.com
  DISCOURSE_SMTP_PORT: 587
  DISCOURSE_SMTP_USER_NAME: <<REDACTED>>
  DISCOURSE_SMTP_PASSWORD: <<REDACTED>>
  #DISCOURSE_SMTP_ENABLE_START_TLS: true           # (opcional, padrão true)
  #DISCOURSE_SMTP_DOMAIN: discourse.example.com    # (obrigatório por alguns provedores)
  DISCOURSE_NOTIFICATION_EMAIL: discourse@cs6991.email    # (endereço para enviar notificações)

  ## Se você adicionou o modelo Let's Encrypt, descomente abaixo para obter um certificado SSL gratuito
  #LETSENCRYPT_ACCOUNT_EMAIL: me@example.com

  ## O endereço CDN http ou https para esta instância do Discourse (configurado para puxar)
  ## veja https://meta.discourse.org/t/14857 para detalhes
  #DISCOURSE_CDN_URL: https://discourse-cdn.example.com

  ## A chave de licença do MaxMind para consulta de endereço IP
  ## veja https://meta.discourse.org/t/-/137387/23 para detalhes
  #DISCOURSE_MAXMIND_LICENSE_KEY: 1234567890123456

  DISCOURSE_RELATIVE_URL_ROOT: '/~cs6991/forum'

## O contêiner Docker é sem estado; todos os dados são armazenados em /shared
volumes:
  - volume:
      host: /var/discourse/shared/standalone
      guest: /shared
  - volume:
      host: /var/discourse/shared/standalone/log/var-log
      guest: /var/log

## Plugins vão aqui
## veja https://meta.discourse.org/t/19157 para detalhes
hooks:
  after_code:
    - exec:
        cd: $home/plugins
        cmd:
          - git clone https://github.com/discourse/docker_manager.git

## Quaisquer comandos personalizados para executar após a construção
run:
  - exec: echo "Início dos comandos personalizados"
  ## Se você quiser definir o e-mail 'De' para seu primeiro registro, descomente e altere:
  ## Após receber o primeiro e-mail de inscrição, comente a linha novamente. Ela só precisa ser executada uma vez.
  #- exec: rails r "SiteSetting.notification_email='info@unconfigured.discourse.org'"
  - exec:
      cd: $home
      cmd:
        - mkdir -p public/~cs6991/forum
        - cd public/~cs6991/forum && ln -s ../../uploads && ln -s ../../backups
  - replace:
     global: true
     filename: /etc/nginx/conf.d/discourse.conf
     from: proxy_pass http://discourse;
     to: |
        rewrite ^/(.*)$ /~cs6991/forum/$1 break;
        proxy_pass http://discourse;
  - replace:
     filename: /etc/nginx/conf.d/discourse.conf
     from: etag off;
     to: |
        etag off;
        location /~cs6991/forum {
           rewrite ^/~cs6991/forum/?(.*)$ /$1;
        }
  - replace:
       filename: /etc/nginx/conf.d/discourse.conf
       from: $proxy_add_x_forwarded_for
       to: $http_your_original_ip_header
       global: true
  - exec: echo "Fim dos comandos personalizados"

Tudo o mais, pelo que pude apurar, parece estar funcionando corretamente - apenas a renderização de uploads está agindo de forma bastante peculiar.

Verifiquei esse comportamento em uma compilação completamente nova - ou seja, rm -rf /var/discourse, apagando completamente o docker e seguindo as instruções de instalação na nuvem + subpasta.

Se houver mais investigações que eu possa realizar, ficarei feliz em dar esses passos. (desculpe pelos links do imgur - ainda não tenho permissão para mais de 2 incorporações de imagem aqui!)

Obrigado!

1 curtida

Algumas informações adicionais – parece que o src já foi removido antes da renderização, pois está faltando no banco de dados de produção:

# sudo -u postgres psql discourse
discourse=# select * from posts where id=13;
 id | user_id | topic_id | post_number |                             raw                             |                                                  cooked                                                  |        created_at         |        updated_at         | reply_to_post_number | reply_count | quote_count |         deleted_at         | off_topic_count | like_count | incoming_link_count | bookmark_count | score | reads | post_type | sort_order | last_editor_id | hidden | hidden_reason_id | notify_moderators_count | spam_count | illegal_count | inappropriate_count |      last_version_at       | user_deleted | reply_to_user_id | percent_rank | notify_user_count | like_score | deleted_by_id | edit_reason | word_count | version | cook_method | wiki |          baked_at          | baked_version | hidden_at | self_edits | reply_quoted | via_email | raw_email | public_version | action_code | locked_by_id | image_upload_id
----+---------+----------+-------------+-------------------------------------------------------------+----------------------------------------------------------------------------------------------------------+---------------------------+---------------------------+----------------------+-------------+-------------+----------------------------+-----------------+------------+---------------------+----------------+-------+-------+-----------+------------+----------------+--------+------------------+-------------------------+------------+---------------+---------------------+----------------------------+--------------+------------------+--------------+-------------------+------------+---------------+-------------+------------+---------+-------------+------+----------------------------+---------------+-----------+------------+--------------+-----------+-----------+----------------+-------------+--------------+-----------------
 13 |       1 |        7 |           2 | ![ferris|690x459](upload://5YA5Y9vjz0iQmn2DErtUBrHCKng.png) | <p><img src="" alt="ferris" data-base62-sha1="5YA5Y9vjz0iQmn2DErtUBrHCKng" width="690" height="459"></p> | 2022-09-01 19:30:38.97281 | 2022-09-01 19:30:38.97281 |                      |           0 |           0 | 2022-09-01 19:47:34.612042 |               0 |          0 |                   0 |              0 |   0.2 |     1 |         1 |          2 |              1 | f      |                  |                       0 |          0 |             0 |                   0 | 2022-09-01 19:30:38.993775 | f            |                  |          0.5 |                 0 |          0 |             1 |             |          5 |       1 |           1 | f    | 2022-09-01 19:30:38.972751 |             2 |           |          0 | f            | f         |           |              1 |             |              |

Também da aba de rede criando uma resposta com uma imagem:

raw	"Minha+imagem+é+inserida+a+seguir:\n\n![ferris|690x459](upload://5YA5Y9vjz0iQmn2DErtUBrHCKng.png)\n\n\nA+imagem+está+acima."
unlist_topic	"false"
category	"4"
topic_id	"7"
is_warning	"false"
archetype	"regular"
typing_duration_msecs	"7500"
composer_open_duration_msecs	"14116"
featured_link	""
shared_draft	"false"
draft_key	"topic_7"
image_sizes[https://cgi.cse.unsw.edu.au/~cs6991/forum/uploads/default/original/1X/29e403dabcfee32379629fb6d844354193e278ba.png][width]	"1200"
image_sizes[https://cgi.cse.unsw.edu.au/~cs6991/forum/uploads/default/original/1X/29e403dabcfee32379629fb6d844354193e278ba.png][height]	"800"
nested_post	"true"

ou a string de consulta bruta, se preferir:

raw=Minha+imagem+é+inserida+a+seguir%3A%0A%0A!%5Bferris%7C690x459%5D(upload%3A%2F%2F5YA5Y9vjz0iQmn2DErtUBrHCKng.png)%0A%0A%0AA+imagem+está+acima.%26unlist_topic=false%26category=4%26topic_id=7%26is_warning=false%26archetype=regular%26typing_duration_msecs=7500%26composer_open_duration_msecs=14116%26featured_link=%26shared_draft=false%26draft_key=topic_7%26image_sizes%5Bhttps%3A%2F%2Fcgi.cse.unsw.edu.au%2F~cs6991%2Fforum%2Fuploads%2Fdefault%2Foriginal%2F1X%2F29e403dabcfee32379629fb6d844354193e278ba.png%5D%5Bwidth%5D=1200%26image_sizes%5Bhttps%3A%2F%2Fcgi.cse.unsw.edu.au%2F~cs6991%2Fforum%2Fuploads%2Fdefault%2Foriginal%2F1X%2F29e403dabcfee32379629fb6d844354193e278ba.png%5D%5Bheight%5D=800%26nested_post=true

a resposta parece ecoar a postagem de volta, e você pode ver que o src já foi removido naquele ponto:

{
  "action": "create_post",
  "post": {
    "id": 16,
    "name": null,
    "username": "z.kologlu",
    "avatar_template": "/~cs6991/forum/letter_avatar_proxy/v4/letter/z/b9bd4f/{size}.png",
    "created_at": "2022-09-02T05:37:25.680Z",
    "cooked": "\u003cp\u003eMinha imagem é inserida a seguir:\u003c/p\u003e\\n\u003cp\u003e\u003cimg src=\"\" alt=\"ferris\" data-base62-sha1=\"5YA5Y9vjz0iQmn2DErtUBrHCKng\" width=\"690\" height=\"459\"\u003e\u003c/p\u003e\\n\u003cp\u003eA imagem está acima.\u003c/p\u003e",
    "post_number": 5,
    "post_type": 1,
    "updated_at": "2022-09-02T05:37:25.680Z",
    "reply_count": 0,
    "reply_to_post_number": null,
    "quote_count": 0,
    "incoming_link_count": 0,
    "reads": 0,
    "readers_count": 0,
    "score": 0,
    "yours": true,
    "topic_id": 7,
    "topic_slug": "welcome-to-discourse",
    "display_username": null,
    "primary_group_name": null,
    "flair_name": null,
    "flair_url": null,
    "flair_bg_color": null,
    "flair_color": null,
    "version": 1,
    "can_edit": true,
    "can_delete": true,
    "can_recover": false,
    "can_wiki": true,
    "user_title": null,
    "bookmarked": false,
    "raw": "Minha imagem é inserida a seguir:\n\n![ferris|690x459](upload://5YA5Y9vjz0iQmn2DErtUBrHCKng.png)\n\n\nA imagem está acima.",
    "actions_summary": [
      {
        "id": 3,
        "can_act": true
      },
      {
        "id": 4,
        "can_act": true
      },
      {
        "id": 8,
        "can_act": true
      },
      {
        "id": 7,
        "can_act": true
      }
    ],
    "moderator": false,
    "admin": true,
    "staff": true,
    "user_id": 1,
    "draft_sequence": 12,
    "hidden": false,
    "trust_level": 1,
    "deleted_at": null,
    "user_deleted": false,
    "edit_reason": null,
    "can_view_edit_history": true,
    "wiki": false,
    "reviewable_id": null,
    "reviewable_score_count": 0,
    "reviewable_score_pending_count": 0
  },
  "success": true
}

Tracked this issue down further to this function: discourse/app/models/post.rb at main · discourse/discourse · GitHub

Modified my local function:

  def cook(raw, opts = {})
    Rails.logger.info("Cooking post with raw: #{raw}")
    # For some posts, for example those imported via RSS, we support raw HTML. In that
    # case we can skip the rendering pipeline.
    return raw if cook_method == Post.cook_methods[:raw_html]

    options = opts.dup
    options[:cook_method] = cook_method

    post_user = self.user
    options[:user_id] = post_user.id if post_user
    options[:omit_nofollow] = true if omit_nofollow?

    if self.with_secure_media?
      each_upload_url do |url|
        uri = URI.parse(url)
        if FileHelper.is_supported_media?(File.basename(uri.path))
          raw = raw.sub(
            url, Rails.application.routes.url_for(
              controller: "uploads", action: "show_secure", path: uri.path[1..-1], host: Discourse.current_hostname
            )
          )
        end
      end
    end

    cooked = post_analyzer.cook(raw, options)

    Rails.logger.info("Cooked into: #{cooked}")

    new_cooked = Plugin::Filter.apply(:after_post_cook, self, cooked)

    if post_type == Post.types[:regular]
      if new_cooked != cooked && new_cooked.blank?
        Rails.logger.debug("Plugin is blanking out post: #{self.url}\nraw: #{raw}")
      elsif new_cooked.blank?
        Rails.logger.debug("Blank post detected post: #{self.url}\nraw: #{raw}")
      end
    end

    Rails.logger.info("New cooked into: #{new_cooked}")

    new_cooked
  end

Output:

Completed 200 OK in 335ms (Views: 0.4ms | ActiveRecord: 0.0ms | Allocations: 78316)
done
done
Cooking post with raw: ![ferris|690x459](upload://5YA5Y9vjz0iQmn2DErtUBrHCKng.png)
Started POST "/~cs6991/forum/presence/update" for 127.0.0.1 at 2022-09-02 05:55:33 +0000
Processing by PresenceController#update as */*
  Parameters: {"client_id"=>"16308337827949548cb8b156301a493b", "leave_channels"=>["/discourse-presence/reply/7"]}
Completed 200 OK in 19ms (Views: 0.2ms | ActiveRecord: 0.0ms | Allocations: 6182)
done
Cooked into: <p><img src="" alt="ferris" data-base62-sha1="5YA5Y9vjz0iQmn2DErtUBrHCKng" width="690" height="459"></p>
New cooked into: <p><img src="" alt="ferris" data-base62-sha1="5YA5Y9vjz0iQmn2DErtUBrHCKng" width="690" height="459"></p>
done

Consegui rastrear os uploads de postagens até esta linha exata:

https://github.com/discourse/discourse/blob/main/app/assets/javascripts/pretty-text/addon/sanitizer.js#L23

em particular,

  // relative urls
  if (/^\\/[\\w\\.\\-]+/i.test(href)) {
    return href;
  }

falha porque, como a URL do meu fórum está sendo servida a partir de um diretório web de usuário do Apache (sobre o qual não tenho controle), ela começa com um ~, o que quebra essa regex.
Confirmei que modificar a classe de caracteres para incluir ~ (como [\\w\\.\\-~]) corrige os uploads de postagens, mas, dolorosamente, os uploads de avatares ainda estão quebrados!

1 curtida

…e a outra regex quebrada:

mesma situação – precisa de um ~ na classe de caracteres
o que corrige meus avatares

1 curtida

Se o Core não estiver tão interessado em um PR, você provavelmente poderia resolver isso permanentemente com um plugin personalizado para seu site específico.

1 curtida