diff --git a/Gemfile b/Gemfile index d64bfce8..143a8105 100644 --- a/Gemfile +++ b/Gemfile @@ -12,7 +12,7 @@ gem 'thor', '~> 0.20' gem 'hamlit-rails', '~> 0.2' gem 'pg', '~> 1.2.3' gem 'makara', '~> 0.4' -gem 'pghero', '~> 2.4.2' +gem 'pghero', '~> 2.7.0' gem 'dotenv-rails', '~> 2.7' gem 'aws-sdk-s3', '~> 1.41', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 0016fb70..16959634 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -401,7 +401,7 @@ GEM equatable (~> 0.5.0) tty-color (~> 0.4.0) pg (1.2.3) - pghero (2.4.2) + pghero (2.7.0) activerecord (>= 5) pkg-config (1.3.7) premailer (1.11.1) @@ -722,7 +722,7 @@ DEPENDENCIES paperclip-av-transcoder (~> 0.6) parallel_tests (~> 2.29) pg (~> 1.2.3) - pghero (~> 2.4.2) + pghero (~> 2.7.0) pkg-config (~> 1.3) posix-spawn! premailer-rails diff --git a/app/controllers/admin/link_blocks_controller.rb b/app/controllers/admin/link_blocks_controller.rb new file mode 100644 index 00000000..50ac5c88 --- /dev/null +++ b/app/controllers/admin/link_blocks_controller.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Admin + class LinkBlocksController < BaseController + before_action :set_link_block, only: [:show, :destroy] + + def index + authorize :link_block, :index? + @link_blocks = LinkBlock.page(params[:page]) + end + + def new + authorize :link_block, :create? + @link_block = LinkBlock.new + end + + def create + authorize :link_block, :create? + + @link_block = LinkBlock.new(resource_params) + + if @link_block.save + log_action :create, @link_block + redirect_to admin_link_blocks_path, notice: I18n.t('admin.link_blocks.created_msg') + else + render :new + end + end + + def destroy + authorize @link_block, :destroy? + @link_block.destroy! + log_action :destroy, @link_block + redirect_to admin_link_blocks_path, notice: I18n.t('admin.link_blocks.destroyed_msg') + end + + private + + def set_link_block + @link_block = LinkBlock.find(params[:id]) + end + + def resource_params + params.require(:link_block).permit(:link) + end + end +end diff --git a/app/javascript/gabsocial/components/table.js b/app/javascript/gabsocial/components/table.js new file mode 100644 index 00000000..9e8a4cdf --- /dev/null +++ b/app/javascript/gabsocial/components/table.js @@ -0,0 +1,58 @@ +import React from 'react' +import PropTypes from 'prop-types' +import Text from './text' +import TableColumnHeader from './table_column_header' +import TableRow from './table_row' + +class Table extends React.PureComponent { + + render() { + const { + id, + columns, + rows, + } = this.props + + return ( + + { + Array.isArray(columns) && + + { + columns.map((column, i) => ( + + )) + } + + } + { + Array.isArray(rows) && + rows.map((row, i) => ( + + )) + } +
+ ) + } + +} + +Table.propTypes = { + id: PropTypes.string, + columns: PropTypes.array, + rows: PropTypes.array, +} + +export default Table \ No newline at end of file diff --git a/app/javascript/gabsocial/components/table_column_header.js b/app/javascript/gabsocial/components/table_column_header.js new file mode 100644 index 00000000..77f20beb --- /dev/null +++ b/app/javascript/gabsocial/components/table_column_header.js @@ -0,0 +1,46 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { CX } from '../constants' +import Text from './text' + +class TableColumnHeader extends React.PureComponent { + + render() { + const { + column, + total, + index, + } = this.props + + const isLast = index === total - 1 + const classes = CX({ + d: 1, + px15: 1, + py10: 1, + borderRight1PX: !isLast, + borderColorSecondary: !isLast, + }) + + const style = { + width: `${100 / total}%` + } + + return ( + + + {column} + + + ) + } + +} + +TableColumnHeader.propTypes = { + column: PropTypes.object, + index: PropTypes.number, + total: PropTypes.number, + tableId: PropTypes.string, +} + +export default TableColumnHeader \ No newline at end of file diff --git a/app/javascript/gabsocial/components/table_row.js b/app/javascript/gabsocial/components/table_row.js new file mode 100644 index 00000000..de3b918d --- /dev/null +++ b/app/javascript/gabsocial/components/table_row.js @@ -0,0 +1,63 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { CX } from '../constants' +import Text from './text' + +class TableRow extends React.PureComponent { + + render() { + const { + id, + row, + total, + index, + } = this.props + + if (!Array.isArray(row)) return null + + const isLast = index === total - 1 + const classes = CX({ + d: 1, + flexRow: 1, + borderBottom1PX: !isLast, + borderColorSecondary: !isLast, + }) + + const style = { + width: `${100 / row.length}%` + } + + return ( + + { + row.map((item, i) => { + const itemClasses = CX({ + d: 1, + px15: 1, + py10: 1, + borderRight1PX: i !== row.length - 1, + borderColorSecondary: i !== row.length - 1, + }) + return ( + + + {row[i]} + + + ) + }) + } + + ) + } + +} + +TableRow.propTypes = { + row: PropTypes.object, + tableId: PropTypes.string, + total: PropTypes.number, + index: PropTypes.number, +} + +export default TableRow \ No newline at end of file diff --git a/app/javascript/gabsocial/features/about/california_consumer_protection.js b/app/javascript/gabsocial/features/about/california_consumer_protection.js new file mode 100644 index 00000000..ea3b5fd1 --- /dev/null +++ b/app/javascript/gabsocial/features/about/california_consumer_protection.js @@ -0,0 +1,378 @@ +import React from 'react' +import Block from '../../components/block' +import Button from '../../components/button' +import Divider from '../../components/divider' +import Heading from '../../components/heading' +import Text from '../../components/text' +import Table from '../../components/table' + +export default class CaliforniaConsumerProtection extends React.PureComponent { + + render() { + + return ( +
+ +
+ GAB AI INC +
+ Privacy Policy for California Residents + Effective Date: 2 December 2020 + Last Updated on: 2 December 2020 +
+ + This Privacy Policy for California Residents supplements the information contained in the Company's privacy policy and applies solely to all visitors, users, and others who reside in the State of California ("consumers" or "you"). We adopt this notice to comply with the California Consumer Privacy Act of 2018 (CCPA) and any terms defined in the CCPA have the same meaning when used in this Policy. + +
+ Information We Collect + We collect information that identifies, relates to, describes, references, is reasonably capable of being associated with, or could reasonably be linked, directly or indirectly, with a particular consumer, household, or device ("personal information"). Personal information does not include: + +
    +
  • + Publicly available information from government records. +
  • +
  • + Deidentified or aggregated consumer information. +
  • +
  • + Information excluded from the CCPA's scope, like: + +
      +
    • + health or medical information covered by the Health Insurance Portability and Accountability Act of 1996 (HIPAA) and the California Confidentiality of Medical Information Act (CMIA), clinical trial data, or other qualifying research data; +
    • +
    • + personal information covered by certain sector-specific privacy laws, including the Fair Credit Reporting Act (FCRA), the Gramm-Leach-Bliley Act (GLBA) or California Financial Information Privacy Act (FIPA), and the Driver's Privacy Protection Act of 1994. +
    • +
    +
  • +
+ + In particular, we have collected the following categories of personal information from consumers within the last twelve (12) months: + +
+ + + We obtain the categories of personal information listed above from the following categories of sources: +
    +
  • + Directly from you. For example, from forms you complete, video, photos, and text information which you post, products and services you purchase, and communications you make with us. +
  • +
  • + Indirectly from you. For example, from observing your actions on our Website. +
  • +
+ +
+ Use of Personal Information + We do not sell user information. We may use or disclose the personal information we collect for one or more of the following purposes: +
    +
  • + To fulfill or meet the reason you provided the information. For example, if you share your name and contact information to request a price quote or ask a question about our products or services, we will use that personal information to respond to your inquiry. If you provide your personal information to purchase a product or service, we will use that information to process your payment and facilitate delivery. We may also save your information to facilitate new product orders or process returns. +
  • +
  • + To provide, support, personalize, and develop our Website, products, and services. +
  • +
  • + To create, maintain, customize, and secure your account with us. +
  • +
  • + To process your requests, purchases, transactions, and payments and prevent transactional fraud. +
  • +
  • + To provide you with support and to respond to your inquiries, including to investigate and address your concerns and monitor and improve our responses. +
  • +
  • + To personalize your Website experience and to deliver content and product and service offerings relevant to your interests, including targeted offers and ads through our Website, third-party sites, and via email or text message (with your consent, where required by law). +
  • +
  • + To help maintain the safety, security, and integrity of our Website, products and services, databases and other technology assets, and business. +
  • +
  • + For testing, research, analysis, and product development, including to develop and improve our Website, products, and services. +
  • +
  • + To respond to law enforcement requests and as required by applicable law, court order, or governmental regulations. +
  • +
  • + As described to you when collecting your personal information or as otherwise set forth in the CCPA. +
  • +
  • + To evaluate or conduct a merger, divestiture, restructuring, reorganization, dissolution, or other sale or transfer of some or all of our assets, whether as a going concern or as part of bankruptcy, liquidation, or similar proceeding, in which personal information held by us about our Website and our users is among the assets transferred. +
  • +
+ + We will not collect additional categories of personal information or use the personal information we collected for materially different, unrelated, or incompatible purposes without providing you notice. + +
+ Sharing Personal Information + We may share your personal information by disclosing it to a third party for a business purpose. We only make these business purpose disclosures under written contracts that describe the purposes, require the recipient to keep the personal information confidential, and prohibit using the disclosed information for any purpose except performing the contract. In the preceding twelve (12) months, Company has disclosed personal information for a business purpose to the categories of third parties indicated in the chart below. + We do not sell your personal information. For more on your personal information sale rights, see Personal Information Sales Opt-Out and Opt-In Rights. + +
+ + Reselling Personal Information + The CCPA prohibits a third party from reselling personal information unless you have received explicit notice and an opportunity to opt-out of further sales. We do not sell your personal information and to our knowledge none of our service providers sell your personal information. If at any future time we do sell users’ personal information you will be afforded the opportunity to opt-out and we will provide a clear and conspicuous link on Gab.com that will enable you to opt-out. + +
+ Your Rights and Choices + The CCPA provides consumers (California residents) with specific rights regarding their personal information. This section describes your CCPA rights and explains how to exercise those rights. + Right to Know and Data Portability + You have the right to request that we disclose certain information to you about our collection and use of your personal information over the past 12 months (the "right to know"). Once we receive your request and confirm your identity (see Exercising Your Rights to Know or Delete), we will disclose to you: + +
    +
  • + The categories of personal information we collected about you. +
  • +
  • + The categories of sources for the personal information we collected about you. +
  • +
  • + Our business or commercial purpose for collecting or selling that personal information. +
  • +
  • + The categories of third parties with whom we share that personal information. +
  • +
  • + If we sold or disclosed your personal information for a business purpose, two separate lists disclosing: +
      +
    • + sales, identifying the personal information categories that each category of recipient purchased; and +
    • +
    • + disclosures for a business purpose, identifying the personal information categories that each category of recipient obtained. +
    • +
    +
  • +
  • + The specific pieces of personal information we collected about you (also called a data portability request). +
  • +
+ + Right to Delete + You have the right to request that we delete any of your personal information that we collected from you and retained, subject to certain exceptions (the "right to delete"). Once we receive your request and confirm your identity (see Exercising Your Rights to Know or Delete), we will review your request to see if an exception allowing us to retain the information applies. We may deny your deletion request if retaining the information is necessary for us or our service provider(s) to: +
    +
  1. + Complete the transaction for which we collected the personal information, provide a good or service that you requested, take actions reasonably anticipated within the context of our ongoing business relationship with you, fulfill the terms of a written warranty or product recall conducted in accordance with federal law, or otherwise perform our contract with you. +
  2. +
  3. + Detect security incidents, protect against malicious, deceptive, fraudulent, or illegal activity, or prosecute those responsible for such activities. +
  4. +
  5. + Debug products to identify and repair errors that impair existing intended functionality. +
  6. +
  7. + Exercise free speech, ensure the right of another consumer to exercise their free speech rights, or exercise another right provided for by law. +
  8. +
  9. + Comply with the California Electronic Communications Privacy Act (Cal. Penal Code § 1546 et. seq.). +
  10. +
  11. + Engage in public or peer-reviewed scientific, historical, or statistical research in the public interest that adheres to all other applicable ethics and privacy laws, when the information's deletion may likely render impossible or seriously impair the research's achievement, if you previously provided informed consent. +
  12. +
  13. + Enable solely internal uses that are reasonably aligned with consumer expectations based on your relationship with us. +
  14. +
  15. + Comply with a legal obligation. +
  16. +
  17. + Make other internal and lawful uses of that information that are compatible with the context in which you provided it. +
  18. +
+ + We will delete or deidentify personal information not subject to one of these exceptions from our records and will direct our service providers to take similar action. + Exercising Your Rights to Know or Delete + + To exercise your rights to know or delete described above, please submit a request by (preferred) e-mailing us at  + +  ; sending a request to us via our CCPA portal at  + + , or or mailing us at + + + Gab AI Inc.
+ 700 N State Street
+ Clarks Summit, PA 18411
+ United States of America +
+ + Only you, or someone legally authorized to act on your behalf, may make a request to know or delete related to your personal information. + You may also make a request to know or delete on behalf of your child, in which case we will require proof of identity and proof of account ownership. + You may only submit a request to know twice within a 12-month period. Your request to know or delete must: +
    +
  • + Provide sufficient information that allows us to reasonably verify you are the person about whom we collected personal information or an authorized representative. +
  • +
  • + Describe your request with sufficient detail that allows us to properly understand, evaluate, and respond to it. +
  • +
+ + We cannot respond to your request or provide you with personal information if we cannot verify your identity or authority to make the request and confirm the personal information relates to you. + You do not need to create an account with us to submit a request to know or delete. However, we do consider requests made through your password protected account sufficiently verified when the request relates to personal information associated with that specific account. + We will only use personal information provided in the request to verify the requestor's identity or authority to make it. + For instructions on exercising your sale opt-out or opt-in rights, see Personal Information Sales Opt-Out and Opt-In Rights. + + Response Timing and Format + We will confirm receipt of your request within ten (10) business days. If you do not receive confirmation within the 10-day timeframe, please contact us at  + + . + + We endeavor to substantively respond to a verifiable consumer request within forty-five (45) days of its receipt. If we require more time (up to another 45 days), we will inform you of the reason and extension period in writing. + If you have an account with us, we will deliver our written response to that account[ via the contact address associated with that account]. If you do not have an account with us, we will deliver our written response by mail or electronically, at your option. + Any disclosures we provide will only cover the 12-month period preceding our receipt of your request. The response we provide will also explain the reasons we cannot comply with a request, if applicable. For data portability requests, we will select a format to provide your personal information that is readily useable and should allow you to transmit the information from one entity to another entity without hindrance. + We do not charge a fee to process or respond to your verifiable consumer request unless it is excessive, repetitive, or manifestly unfounded. If we determine that the request warrants a fee, we will tell you why we made that decision and provide you with a cost estimate before completing your request. + + Personal Information Sales Opt-Out and Opt-In Rights + If you are age 16 or older, you have the right to direct us to not sell your personal information at any time (the "right to opt-out"). We do not sell the personal information of any user. If in the future we decide to sell personal information, we will afford you with a right to opt-out and will provide an opt-out link in this CCPA Privacy Policy. + +
+ Non-Discrimination + You have a right not to receive discriminatory treatment for exercising your CCPA rights and we will not discriminate against you for exercising any of your CCPA rights. Unless permitted by the CCPA, we will not: +
    +
  • + Deny you goods or services. +
  • +
  • + Charge you different prices or rates for goods or services, including through granting discounts or other benefits, or imposing penalties. +
  • +
  • + Provide you a different level or quality of goods or services. +
  • +
  • + Suggest that you may receive a different price or rate for goods or services or a different level or quality of goods or services. +
  • +
+ +
+ CCPA Rights Request Met + Metrics regarding the consumer rights requests we received from California residents from January 1, 2019 to December 31, 2019 appear in the following chart: +
+ +
+ Other California Privacy Rights + California's "Shine the Light" law (Civil Code Section § 1798.83) permits users of our Website that are California residents to request certain information regarding our disclosure of personal information to third parties for their direct marketing purposes. To make such a request, please write to us at Gab AI Inc., 700 N State Street, Clarks Summit, PA, 18411. + +
+ Changes to Our Privacy Policy + We reserve the right to amend this privacy policy at our discretion and at any time. When we make changes to this privacy policy, we will post the updated notice on the Website and update the notice's effective date. Your continued use of our Website following the posting of changes constitutes your acceptance of such changes. + +
+ Contact Information + If you have any questions or comments about this notice, the ways in which Gab collects and uses your information described here and in the Privacy Policy, your choices and rights regarding such use, or wish to exercise your rights under California law, please do not hesitate to contact us at: + + + Contact form: + If you have any questions or comments about this notice, the ways in which Gab collects and uses your information described here and in the Privacy Policy, your choices and rights regarding such use, or wish to exercise your rights under California law, please do not hesitate to contact us at: + + + + Email: + + + + + Postal Address: + Gab AI Inc.
+ 700 N State Street
+ Clarks Summit, PA 18411
+ Attn: Data Privacy Department +
+ + If you need to access this Policy in an alternative format due to having a disability, please contact  + + + + + + + ) + } + +} diff --git a/app/javascript/gabsocial/features/about/california_consumer_protection_contact.js b/app/javascript/gabsocial/features/about/california_consumer_protection_contact.js new file mode 100644 index 00000000..47bf342e --- /dev/null +++ b/app/javascript/gabsocial/features/about/california_consumer_protection_contact.js @@ -0,0 +1,42 @@ +import React from 'react' +import Block from '../../components/block' +import Button from '../../components/button' +import Divider from '../../components/divider' +import Heading from '../../components/heading' +import Text from '../../components/text' +import Table from '../../components/table' + +export default class CaliforniaConsumerProtectionContact extends React.PureComponent { + + render() { + + return ( +
+ +
+ GAB AI INC +
+ Contact form for Data Privacy +
+ + + Email: + + + +
+
+
+ ) + } + +} diff --git a/app/javascript/gabsocial/features/about/privacy_policy.js b/app/javascript/gabsocial/features/about/privacy_policy.js index e921ec26..af88941a 100644 --- a/app/javascript/gabsocial/features/about/privacy_policy.js +++ b/app/javascript/gabsocial/features/about/privacy_policy.js @@ -278,6 +278,40 @@ export default class PrivacyPolicy extends React.PureComponent { Changes to Our Privacy PolicyIt is our policy to post any changes we make to our privacy policy on this page with a notice that the privacy policy has been updated on the Website home page. If we make material changes to how we treat our users’ personal information, we will notify you through a notice on the Website home page. The date the privacy policy was last revised is identified at the top of the page. You are responsible for ensuring we have an up-to-date active and deliverable email address for you, and for periodically visiting our Website and this privacy policy to check for any changes. +
+ Your California Privacy Rights + If you are a California resident, California law may provide you with additional rights regarding our use of your personal information. To learn more about your California privacy rights, visit our CCPA Privacy Notice at  + + + California's "Shine the Light" law (Civil Code Section § 1798.83) permits users of our Website that are California residents to request certain information regarding our disclosure of personal information to third parties for their direct marketing purposes. To make such a request, please send us an e-mail to  + +  or write to us at + + + Gab AI Inc.
+ 700 N State Street
+ Clarks Summit, PA 18411
+ Attn: Data Privacy Department +
+
Contact InformationTo ask questions or comment about this privacy policy and our privacy practices, contact us at: diff --git a/app/javascript/gabsocial/features/ui/ui.js b/app/javascript/gabsocial/features/ui/ui.js index 778b1fa2..173e2e6c 100644 --- a/app/javascript/gabsocial/features/ui/ui.js +++ b/app/javascript/gabsocial/features/ui/ui.js @@ -57,6 +57,8 @@ import { Assets, BlockedAccounts, BookmarkedStatuses, + CaliforniaConsumerProtection, + CaliforniaConsumerProtectionContact, ChatConversationCreate, ChatConversationRequests, ChatConversationBlockedAccounts, @@ -199,6 +201,8 @@ class SwitchingArea extends React.PureComponent { + + diff --git a/app/javascript/gabsocial/features/ui/util/async_components.js b/app/javascript/gabsocial/features/ui/util/async_components.js index 91a2b2ea..3a706d2e 100644 --- a/app/javascript/gabsocial/features/ui/util/async_components.js +++ b/app/javascript/gabsocial/features/ui/util/async_components.js @@ -7,6 +7,8 @@ export function BlockAccountModal() { return import(/* webpackChunkName: "compon export function BlockedAccounts() { return import(/* webpackChunkName: "features/blocked_accounts" */'../../blocked_accounts') } export function BookmarkedStatuses() { return import(/* webpackChunkName: "features/bookmarked_statuses" */'../../bookmarked_statuses') } export function BoostModal() { return import(/* webpackChunkName: "components/boost_modal" */'../../../components/modal/boost_modal') } +export function CaliforniaConsumerProtection() { return import(/* webpackChunkName: "features/california_consumer_protection" */'../../about/california_consumer_protection') } +export function CaliforniaConsumerProtectionContact() { return import(/* webpackChunkName: "features/california_consumer_protection_contact" */'../../about/california_consumer_protection_contact') } export function ChatConversationBlockedAccounts() { return import(/* webpackChunkName: "features/chat_conversation_blocked_accounts" */'../../chat_conversation_blocked_accounts') } export function ChatConversationCreate() { return import(/* webpackChunkName: "features/chat_conversation_create" */'../../chat_conversation_create') } export function ChatConversationCreateModal() { return import(/* webpackChunkName: "components/chat_conversation_create_modal" */'../../../components/modal/chat_conversation_create_modal') } diff --git a/app/javascript/gabsocial/layouts/about_layout.js b/app/javascript/gabsocial/layouts/about_layout.js index a16a74ae..d1fac708 100644 --- a/app/javascript/gabsocial/layouts/about_layout.js +++ b/app/javascript/gabsocial/layouts/about_layout.js @@ -53,6 +53,10 @@ class AboutLayout extends React.PureComponent { title: 'Terms of Service', to: '/about/tos', }, + { + title: 'California Consumer Protection', + to: '/about/ccpa', + }, ] } diff --git a/app/lib/tag_manager.rb b/app/lib/tag_manager.rb index f9a3ac41..ce5d62f1 100644 --- a/app/lib/tag_manager.rb +++ b/app/lib/tag_manager.rb @@ -22,6 +22,12 @@ class TagManager uri.normalized_host end + def normalize_link(link) + return if link.nil? + uri = Addressable::URI.parse(link) + return "#{uri.normalized_host}#{uri.normalized_path}" + end + def same_acct?(canonical, needle) return true if canonical.casecmp(needle).zero? username, domain = needle.split('@') diff --git a/app/models/concerns/link_normalizable.rb b/app/models/concerns/link_normalizable.rb new file mode 100644 index 00000000..967d40be --- /dev/null +++ b/app/models/concerns/link_normalizable.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module LinkNormalizable + extend ActiveSupport::Concern + + included do + before_validation :normalize_link + end + + private + + def normalize_link + self.link = TagManager.instance.normalize_link(link&.strip) + end +end diff --git a/app/models/link_block.rb b/app/models/link_block.rb new file mode 100644 index 00000000..6766e1b7 --- /dev/null +++ b/app/models/link_block.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: link_blocks +# +# id :bigint(8) not null, primary key +# link :string default(""), not null +# created_at :datetime not null +# updated_at :datetime not null +# + +class LinkBlock < ApplicationRecord + include LinkNormalizable + + validates :link, presence: true, uniqueness: true + + def self.block?(text) + return false if text.nil? + return false if text.length < 1 + + urls = text.scan(FetchLinkCardService::URL_PATTERN).map { |array| Addressable::URI.parse(array[0]).normalize } + url = urls.first + link_for_fetch = TagManager.instance.normalize_link(url) + where(link: link_for_fetch).exists? + end +end \ No newline at end of file diff --git a/app/models/status.rb b/app/models/status.rb index f7538228..40473320 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -290,13 +290,13 @@ class Status < ApplicationRecord end def as_home_timeline(account) - query = where('updated_at > ?', 5.days.ago) + query = where('created_at > ?', 5.days.ago) query.where(visibility: [:public, :unlisted, :private]) query.where(account: [account] + account.following).without_replies end def as_group_timeline(group) - query = where('updated_at > ?', 5.days.ago) + query = where('created_at > ?', 5.days.ago) query.where(group: group).without_replies end diff --git a/app/policies/link_block_policy.rb b/app/policies/link_block_policy.rb new file mode 100644 index 00000000..8b47dc14 --- /dev/null +++ b/app/policies/link_block_policy.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class LinkBlockPolicy < ApplicationPolicy + def index? + admin? + end + + def create? + admin? + end + + def destroy? + admin? + end +end diff --git a/app/services/edit_status_service.rb b/app/services/edit_status_service.rb index 16d7015e..561fee88 100644 --- a/app/services/edit_status_service.rb +++ b/app/services/edit_status_service.rb @@ -25,6 +25,7 @@ class EditStatusService < BaseService return idempotency_duplicate if idempotency_given? && idempotency_duplicate? + validate_links! validate_media! preprocess_attributes! revision_text = prepare_revision_text @@ -89,6 +90,10 @@ class EditStatusService < BaseService raise GabSocial::ValidationError, I18n.t('media_attachments.validations.images_and_video') if @media.size > 1 && hasVideoOrGif end + def validate_links! + raise GabSocial::NotPermittedError if LinkBlock.block?(@text) + end + def language_from_option(str) ISO_639.find(str)&.alpha2 end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 6b77aa9f..5a8eae2b 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -34,6 +34,7 @@ class PostStatusService < BaseService return idempotency_duplicate if idempotency_given? && idempotency_duplicate? + validate_links! validate_media! validate_group! preprocess_attributes! @@ -98,7 +99,7 @@ class PostStatusService < BaseService end def postprocess_status! - LinkCrawlWorker.perform_async(@status.id) unless @status.spoiler_text? + LinkCrawlWorker.perform_async(@status.id) DistributionWorker.perform_async(@status.id) ExpiringStatusWorker.perform_at(@status.expires_at, @status.id) if @status.expires_at && @account.is_pro PollExpirationNotifyWorker.perform_at(@status.poll.expires_at, @status.poll.id) if @status.poll @@ -118,12 +119,17 @@ class PostStatusService < BaseService raise GabSocial::ValidationError, I18n.t('media_attachments.validations.too_many') if @options[:media_ids].size > 4 || @options[:poll].present? + @account.media_attachments.connection.stick_to_master! @media = @account.media_attachments.where(status_id: nil).where(id: @options[:media_ids].take(4).map(&:to_i)) hasVideoOrGif = @media.find(&:video?) || @media.find(&:gifv?) raise GabSocial::ValidationError, I18n.t('media_attachments.validations.images_and_video') if @media.size > 1 && hasVideoOrGif end + def validate_links! + raise GabSocial::NotPermittedError if LinkBlock.block?(@text) + end + def language_from_option(str) ISO_639.find(str)&.alpha2 end diff --git a/app/views/admin/link_blocks/_link_block.html.haml b/app/views/admin/link_blocks/_link_block.html.haml new file mode 100644 index 00000000..845b3be3 --- /dev/null +++ b/app/views/admin/link_blocks/_link_block.html.haml @@ -0,0 +1,5 @@ +%tr + %td + %samp= link_block.link + %td + = table_link_to 'trash', t('admin.link_blocks.delete'), admin_link_block_path(link_block), method: :delete diff --git a/app/views/admin/link_blocks/index.html.haml b/app/views/admin/link_blocks/index.html.haml new file mode 100644 index 00000000..2b834df0 --- /dev/null +++ b/app/views/admin/link_blocks/index.html.haml @@ -0,0 +1,14 @@ +- content_for :page_title do + = t('admin.link_blocks.title') + +.table-wrapper + %table.table + %thead + %tr + %th= t('admin.link_blocks.link') + %th + %tbody + = render @link_blocks + += paginate @link_blocks += link_to t('admin.link_blocks.add_new'), new_admin_link_block_path, class: 'button' diff --git a/app/views/admin/link_blocks/new.html.haml b/app/views/admin/link_blocks/new.html.haml new file mode 100644 index 00000000..cc19d9e0 --- /dev/null +++ b/app/views/admin/link_blocks/new.html.haml @@ -0,0 +1,11 @@ +- content_for :page_title do + = t('.title') + += simple_form_for @link_block, url: admin_link_blocks_path do |f| + = render 'shared/error_messages', object: @link_block + + .fields-group + = f.input :link, wrapper: :with_label, label: t('admin.link_blocks.link') + + .actions + = f.button :button, t('.create'), type: :submit diff --git a/config/locales/en.yml b/config/locales/en.yml index 6b2cfca5..d217b89b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -325,6 +325,16 @@ en: title: Undo domain block for %{domain} undo: Undo undo: Undo domain block + link_blocks: + add_new: Add new + created_msg: Successfully added link to blacklist + delete: Delete + destroyed_msg: Successfully deleted link from blacklist + link: Link + new: + create: Add link + title: New link blacklist entry + title: Link blacklist email_domain_blocks: add_new: Add new created_msg: Successfully added e-mail domain to blacklist diff --git a/config/locales/en_GB.yml b/config/locales/en_GB.yml index 4e3ed84f..60044551 100644 --- a/config/locales/en_GB.yml +++ b/config/locales/en_GB.yml @@ -292,6 +292,16 @@ en_GB: title: Undo domain block for %{domain} undo: Undo undo: Undo domain block + link_blocks: + add_new: Add new + created_msg: Successfully added link to blacklist + delete: Delete + destroyed_msg: Successfully deleted link from blacklist + link: Link + new: + create: Add link + title: New link blacklist entry + title: Link blacklist email_domain_blocks: add_new: Add new created_msg: Successfully added e-mail domain to blacklist diff --git a/config/navigation.rb b/config/navigation.rb index ac6d5714..6881b791 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -31,6 +31,7 @@ SimpleNavigation::Configuration.run do |navigation| s.item :action_logs, safe_join([fa_icon('bars fw'), t('admin.action_logs.title')]), admin_action_logs_url s.item :reports, safe_join([fa_icon('flag fw'), t('admin.reports.title')]), admin_reports_url, highlights_on: %r{/admin/reports} s.item :email_domain_blocks, safe_join([fa_icon('envelope fw'), t('admin.email_domain_blocks.title')]), admin_email_domain_blocks_url, highlights_on: %r{/admin/email_domain_blocks}, if: -> { current_user.admin? } + s.item :link_blocks, safe_join([fa_icon('link fw'), t('admin.link_blocks.title')]), admin_link_blocks_url, highlights_on: %r{/admin/link_blocks}, if: -> { current_user.admin? } end n.item :admin, safe_join([fa_icon('cogs fw'), t('admin.title')]), admin_dashboard_url, if: proc { current_user.staff? } do |s| diff --git a/config/routes.rb b/config/routes.rb index e6b7176c..65408580 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -100,6 +100,7 @@ Rails.application.routes.draw do get '/dashboard', to: 'dashboard#index' resources :email_domain_blocks, only: [:index, :new, :create, :destroy] + resources :link_blocks, only: [:index, :new, :create, :destroy] resources :action_logs, only: [:index] resources :warning_presets, except: [:new] resource :settings, only: [:edit, :update] diff --git a/db/migrate/20201206060226_create_link_blocks.rb b/db/migrate/20201206060226_create_link_blocks.rb new file mode 100644 index 00000000..af47466b --- /dev/null +++ b/db/migrate/20201206060226_create_link_blocks.rb @@ -0,0 +1,10 @@ +class CreateLinkBlocks < ActiveRecord::Migration[5.2] + def change + create_table :link_blocks do |t| + t.string :link, null: false, default: '' + t.timestamps null: false + end + + add_index :link_blocks, :link, unique: true + end +end \ No newline at end of file diff --git a/db/schema.rb b/db/schema.rb index 5d23d675..5414eac9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_12_03_214600) do +ActiveRecord::Schema.define(version: 2020_12_06_060226) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" @@ -394,6 +394,13 @@ ActiveRecord::Schema.define(version: 2020_12_03_214600) do t.index ["user_id"], name: "index_identities_on_user_id" end + create_table "link_blocks", force: :cascade do |t| + t.string "link", default: "", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["link"], name: "index_link_blocks_on_link", unique: true + end + create_table "list_accounts", force: :cascade do |t| t.bigint "list_id", null: false t.bigint "account_id", null: false