lower bound used for threshold tests. */ const BANDS = array( 'lt3k' => 0, 'lt10k' => 0, // Older setting value (before the 3,000 / 5,000 bands). '3k' => 3000, '5k' => 5000, '10k' => 10000, '25k' => 25000, '35k' => 35000, '75k' => 75000, '100k' => 100000, '175k' => 175000, ); /** * Settings. * * @var Settings */ private $s; /** * Cache. * * @var array|null */ private $cache = null; /** * Constructor. * * @param Settings $settings Settings. */ public function __construct( Settings $settings ) { $this->s = $settings; } /** * Markets served (location codes). Defaults to the business location. * * @return string[] */ public function markets() { $m = (array) $this->s->get( 'markets' ); $loc = (string) $this->s->get( 'location' ); if ( $loc && 'OTHER' !== $loc && ! in_array( $loc, $m, true ) ) { $m[] = $loc; } return array_values( array_unique( $m ) ); } /** * Whether the business handles sensitive data (health, kids, or said so). * * @return bool */ public function sensitive() { $ind = (array) $this->s->get( 'industries' ); return (bool) $this->s->get( 'sensitive_data' ) || in_array( 'health', $ind, true ) || in_array( 'kids', $ind, true ); } /** * Whether a comprehensive state law's thresholds are met. * * @param array $t Test. * @param string $asof Date the thresholds are read for (Y-m-d), default today. * @return bool */ private function meets( array $t, $asof = '' ) { $asof = $asof ? $asof : gmdate( 'Y-m-d' ); $band = (string) $this->s->get( 'consumers' ); $n = isset( self::BANDS[ $band ] ) ? self::BANDS[ $band ] : 0; $rev = (bool) $this->s->get( 'revenue_25m' ); $sells = (bool) $this->s->get( 'earns_from_data' ); $small = (bool) $this->s->get( 'sba_small' ); if ( ! empty( $t['from'] ) ) { foreach ( $t['from'] as $date => $later ) { if ( $date <= $asof ) { $t = array_merge( $t, $later ); } } } if ( ! empty( $t['never'] ) ) { return false; } if ( ! empty( $t['sba_exempt'] ) && $small ) { return false; } if ( ! empty( $t['sba'] ) ) { return ! $small; } $count = ( isset( $t['n'] ) && $n >= $t['n'] ) || ( isset( $t['nsell'] ) && $sells && $n >= $t['nsell'] ); if ( ! empty( $t['rev_and'] ) ) { return $rev && $count; } if ( ! empty( $t['sba_unless_sells'] ) ) { return $count && ( ! $small || $sells ); } return $count || ( ! empty( $t['rev'] ) && $rev ) || ( ! empty( $t['sensitive'] ) && $this->sensitive() ); } /** * Laws that apply now, and those that will apply once they take effect. * * @return array{now: array, soon: array} */ public function laws() { if ( null !== $this->cache ) { return $this->cache; } $markets = $this->markets(); $ind = (array) $this->s->get( 'industries' ); $today = gmdate( 'Y-m-d' ); $us = (bool) array_filter( $markets, static function ( $m ) { return 0 === strpos( $m, 'US-' ); } ); $now = array(); $soon = array(); foreach ( Catalog::states() as $code => $l ) { if ( ! in_array( $code, $markets, true ) ) { continue; } if ( ! $this->meets( $l['test'], max( $today, $l['effective'] ) ) ) { // Lower thresholds on the way (e.g. Delaware 2027)? $later = ''; foreach ( isset( $l['test']['from'] ) ? array_keys( $l['test']['from'] ) : array() as $date ) { if ( $date > $today && $this->meets( $l['test'], $date ) ) { $later = $date; break; } } if ( '' === $later ) { continue; } $l['effective'] = $later; } $row = array( 'id' => $code, 'short' => $l['law'], 'cite' => $l['cite'], 'effective' => $l['effective'], 'gpc' => $l['gpc'] && $l['gpc'] <= $today, 'appeal' => $l['appeal'], 'optin' => $l['optin'], 'annual' => $l['annual'], 'incentive' => $l['incentive'], 'kind' => 'state', ); if ( $l['effective'] > $today ) { $soon[ $code ] = $row; } else { $now[ $code ] = $row; } } // Health data laws: health businesses, or anyone who says they handle sensitive data // (e.g. a restaurant that keeps guests' allergy notes). $health = in_array( 'health', $ind, true ) || (bool) $this->s->get( 'sensitive_data' ); foreach ( Catalog::others() as $id => $o ) { $in = '*US' === $o['market'] ? $us : in_array( $o['market'], $markets, true ); if ( ! $in ) { continue; } if ( ! empty( $o['industry'] ) && ! in_array( $o['industry'], $ind, true ) ) { continue; } if ( ! empty( $o['health'] ) && ! $health ) { continue; } $now[ $id ] = array( 'id' => $id, 'short' => $o['short'], 'cite' => $o['cite'], 'kind' => ! empty( $o['health'] ) ? 'health' : 'other', 'gpc' => false, 'appeal' => false, ); } $this->cache = array( 'now' => $now, 'soon' => $soon, ); return $this->cache; } /** * Laws (short names) that legally require something of this business. * * Requirements: website_policy, dnt, third_party, rights, appeal, gpc, annual, choices, * sensitive, health_policy, incentive, kids, finance, legal_basis. * * @param string $req Requirement key. * @return string[] */ public function required_by( $req ) { $laws = $this->laws()['now']; $out = array(); foreach ( $this->required_ids( $req ) as $id ) { $out[] = $laws[ $id ]['short']; } return $out; } /** * Ids (Catalog keys) of the laws that require something of this business. * * @param string $req Requirement key (see required_by()). * @return string[] */ public function required_ids( $req ) { $laws = $this->laws()['now']; $state = array_filter( $laws, static function ( $l ) { return 'state' === $l['kind']; } ); $pick = static function ( array $ids ) use ( $laws ) { $out = array(); foreach ( $ids as $id ) { if ( isset( $laws[ $id ] ) ) { $out[] = $id; } } return $out; }; $where = static function ( $field ) use ( $state ) { $out = array(); foreach ( $state as $id => $l ) { if ( ! empty( $l[ $field ] ) ) { $out[] = $id; } } return $out; }; $all_state = array_keys( $state ); switch ( $req ) { case 'website_policy': return array_merge( $pick( array( 'CALOPPA', 'DOPPA', 'NV603A', 'GDPR', 'UK_GDPR', 'PIPEDA', 'COPPA', 'GLBA' ) ), $all_state ); case 'dnt': // CalOPPA / DOPPA: say how the site responds to "Do Not Track"; CCPA regulations: explain opt-out preference signals. return $pick( array( 'CALOPPA', 'DOPPA', 'US-CA' ) ); case 'third_party': // Disclose third parties that collect personal data through the site. return array_merge( $pick( array( 'CALOPPA', 'DOPPA', 'NV603A', 'GDPR', 'UK_GDPR', 'PIPEDA', 'COPPA' ) ), $all_state ); case 'rights': return array_merge( $all_state, $pick( array( 'GDPR', 'UK_GDPR', 'PIPEDA', 'WA_MHMDA', 'NV_SB370' ) ) ); case 'appeal': return array_merge( $where( 'appeal' ), $pick( array( 'WA_MHMDA' ) ) ); case 'gpc': return $where( 'gpc' ); case 'annual': return $where( 'annual' ); case 'choices': return $all_state; case 'notice_collection': // CCPA regulations ยง 7012: a notice at collection, linked where the information is collected. return $pick( array( 'US-CA' ) ); case 'sensitive': return array_merge( $where( 'optin' ), $pick( array( 'WA_MHMDA', 'NV_SB370', 'CT_CHD', 'GDPR', 'UK_GDPR' ) ) ); case 'health_policy': return $pick( array( 'WA_MHMDA', 'NV_SB370' ) ); case 'incentive': return $where( 'incentive' ); case 'kids': return $pick( array( 'COPPA' ) ); case 'finance': return $pick( array( 'GLBA' ) ); case 'legal_basis': return $pick( array( 'GDPR', 'UK_GDPR' ) ); } return array(); } /** * Severity and explanation suffix for a requirement. * * @param string $req Requirement key. * @param string $required Severity when the law requires it. * @return array{0:string,1:string,2:bool,3:string[]} severity, sentence, required?, law ids */ public function severity( $req, $required = 'warning' ) { $ids = array_values( array_unique( $this->required_ids( $req ) ) ); $by = array_values( array_unique( $this->required_by( $req ) ) ); if ( $by ) { $list = count( $by ) > 3 ? implode( ', ', array_slice( $by, 0, 3 ) ) . ' ' . sprintf( /* translators: %d: number of other laws. */ _n( '+%d more', '+%d more', count( $by ) - 3, 'goconsent' ), count( $by ) - 3 ) : implode( ', ', $by ); /* translators: %s: law names. */ return array( $required, sprintf( __( 'Required by %s.', 'goconsent' ), $list ), true, $ids ); } return array( 'info', __( 'Not legally required for your business today; recommended.', 'goconsent' ), false, array() ); } }