settings = $settings; } /** * Registers the shortcode. */ public function register() { add_shortcode( 'orbitstem_business', array( $this, 'shortcode' ) ); } /** * Business types offered in Settings (schema.org LocalBusiness subtypes). * * @return array */ public static function types() { return array( 'LocalBusiness' => __( 'Local business (general)', 'orbitstem-seo' ), 'Store' => __( 'Shop or store', 'orbitstem-seo' ), 'Restaurant' => __( 'Restaurant', 'orbitstem-seo' ), 'CafeOrCoffeeShop' => __( 'Café', 'orbitstem-seo' ), 'BarOrPub' => __( 'Bar or pub', 'orbitstem-seo' ), 'Bakery' => __( 'Bakery', 'orbitstem-seo' ), 'BeautySalon' => __( 'Beauty salon', 'orbitstem-seo' ), 'HairSalon' => __( 'Hair salon', 'orbitstem-seo' ), 'HealthAndBeautyBusiness' => __( 'Health and beauty', 'orbitstem-seo' ), 'Dentist' => __( 'Dentist', 'orbitstem-seo' ), 'MedicalClinic' => __( 'Medical clinic', 'orbitstem-seo' ), 'Physician' => __( 'Doctor', 'orbitstem-seo' ), 'LegalService' => __( 'Lawyer or legal service', 'orbitstem-seo' ), 'AccountingService' => __( 'Accountant', 'orbitstem-seo' ), 'RealEstateAgent' => __( 'Real estate agent', 'orbitstem-seo' ), 'AutoRepair' => __( 'Car repair', 'orbitstem-seo' ), 'HomeAndConstructionBusiness' => __( 'Builder, plumber or electrician', 'orbitstem-seo' ), 'LodgingBusiness' => __( 'Hotel or lodging', 'orbitstem-seo' ), 'ExerciseGym' => __( 'Gym', 'orbitstem-seo' ), 'ProfessionalService' => __( 'Agency or professional service', 'orbitstem-seo' ), 'EducationalOrganization' => __( 'School or training centre', 'orbitstem-seo' ), ); } /** * Default "local" settings group. * * @return array */ public static function defaults() { return array( 'enabled' => false, 'type' => 'LocalBusiness', 'name' => '', 'phone' => '', 'email' => '', 'street' => '', 'city' => '', 'region' => '', 'postal' => '', 'country' => '', 'lat' => '', 'lng' => '', 'price_range' => '', 'hours' => array(), ); } /** * Cleans the "local" settings group. * * @param mixed $input Raw. * @return array */ public static function sanitize( $input ) { $input = is_array( $input ) ? $input : array(); $clean = self::defaults(); foreach ( array( 'name', 'phone', 'street', 'city', 'region', 'postal', 'price_range' ) as $key ) { $clean[ $key ] = isset( $input[ $key ] ) ? mb_substr( Settings::sanitize_text( $input[ $key ] ), 0, 200 ) : ''; } $clean['enabled'] = ! empty( $input['enabled'] ); $clean['type'] = isset( $input['type'] ) && isset( self::types()[ $input['type'] ] ) ? $input['type'] : 'LocalBusiness'; $clean['email'] = isset( $input['email'] ) ? (string) sanitize_email( $input['email'] ) : ''; $clean['country'] = isset( $input['country'] ) ? strtoupper( substr( preg_replace( '/[^A-Za-z]/', '', (string) $input['country'] ), 0, 2 ) ) : ''; foreach ( array( 'lat' => 90, 'lng' => 180, ) as $key => $max ) { $value = isset( $input[ $key ] ) ? trim( (string) $input[ $key ] ) : ''; $clean[ $key ] = is_numeric( $value ) && abs( (float) $value ) <= $max ? (string) round( (float) $value, 6 ) : ''; } $hours = isset( $input['hours'] ) && is_array( $input['hours'] ) ? $input['hours'] : array(); foreach ( self::DAYS as $day ) { $row = isset( $hours[ $day ] ) && is_array( $hours[ $day ] ) ? $hours[ $day ] : array(); $ok = function ( $time ) { return is_string( $time ) && preg_match( '/^([01]\d|2[0-3]):[0-5]\d$/', $time ) ? $time : ''; }; $open = $ok( isset( $row['open'] ) ? $row['open'] : '' ); $close = $ok( isset( $row['close'] ) ? $row['close'] : '' ); if ( '' !== $open && '' !== $close ) { $clean['hours'][ $day ] = array( 'open' => $open, 'close' => $close, ); } } return $clean; } /** * Current details. * * @return array */ public function details() { $stored = $this->settings->get( 'local' ); return array_merge( self::defaults(), is_array( $stored ) ? $stored : array() ); } /** * What is still missing for a useful listing, in plain words. * * @return string[] */ public function missing() { $d = $this->details(); $out = array(); if ( '' === $d['street'] || '' === $d['city'] || '' === $d['country'] ) { $out[] = __( 'Add the street, city and country so search engines can show where you are.', 'orbitstem-seo' ); } if ( '' === $d['phone'] ) { $out[] = __( 'Add a phone number; it can appear as a call button in search results.', 'orbitstem-seo' ); } if ( ! $d['hours'] ) { $out[] = __( 'Add opening hours so searchers know when you are open.', 'orbitstem-seo' ); } return $out; } /** * LocalBusiness node for the front page, or null when off. * * @param string $publisher_id Organization or Person @id. * @param string $logo Logo URL ('' when none). * @return array|null */ public function node( $publisher_id, $logo ) { $d = $this->details(); if ( ! $d['enabled'] ) { return null; } $home = home_url( '/' ); $node = array( '@type' => $d['type'], '@id' => $home . '#localbusiness', // The stored site name is HTML-escaped ("Tom & Jerry's"); schema wants plain text. 'name' => '' !== $d['name'] ? $d['name'] : \Orbitstem\SEO\Content\TemplateEngine::plain( get_bloginfo( 'name' ) ), 'url' => $home, ); if ( '' !== $logo ) { $node['image'] = $logo; } if ( '#organization' === substr( $publisher_id, -13 ) ) { $node['parentOrganization'] = array( '@id' => $publisher_id ); } $address = array_filter( array( '@type' => 'PostalAddress', 'streetAddress' => $d['street'], 'addressLocality' => $d['city'], 'addressRegion' => $d['region'], 'postalCode' => $d['postal'], 'addressCountry' => $d['country'], ) ); if ( count( $address ) > 1 ) { $node['address'] = $address; } foreach ( array( 'phone' => 'telephone', 'email' => 'email', 'price_range' => 'priceRange', ) as $key => $prop ) { if ( '' !== $d[ $key ] ) { $node[ $prop ] = $d[ $key ]; } } if ( '' !== $d['lat'] && '' !== $d['lng'] ) { $node['geo'] = array( '@type' => 'GeoCoordinates', 'latitude' => (float) $d['lat'], 'longitude' => (float) $d['lng'], ); } foreach ( $d['hours'] as $day => $times ) { $node['openingHoursSpecification'][] = array( '@type' => 'OpeningHoursSpecification', 'dayOfWeek' => 'https://schema.org/' . $day, 'opens' => $times['open'], 'closes' => $times['close'], ); } return $node; } /** * [orbitstem_business]: address, phone, email and hours for visitors. * * @return string */ public function shortcode() { $d = $this->details(); if ( ! $d['enabled'] ) { return ''; } $lines = array_filter( array( $d['street'], trim( $d['postal'] . ' ' . $d['city'] ), $d['region'], $d['country'] ) ); $out = '
'; $out .= '

' . esc_html( '' !== $d['name'] ? $d['name'] : \Orbitstem\SEO\Content\TemplateEngine::plain( get_bloginfo( 'name' ) ) ) . '

'; if ( $lines ) { $out .= '

' . implode( '
', array_map( 'esc_html', $lines ) ) . '

'; // A plain link: nothing is loaded from the map service unless a visitor clicks it. $query = '' !== $d['lat'] && '' !== $d['lng'] ? $d['lat'] . ',' . $d['lng'] : implode( ', ', $lines ); $out .= '

' . esc_html__( 'Get directions', 'orbitstem-seo' ) . '

'; } if ( '' !== $d['phone'] ) { $out .= '

' . esc_html( $d['phone'] ) . '

'; } if ( '' !== $d['email'] ) { $out .= '

' . esc_html( $d['email'] ) . '

'; } if ( $d['hours'] ) { $out .= ''; foreach ( self::DAYS as $day ) { $label = date_i18n( 'l', strtotime( 'next ' . $day ) ); $times = isset( $d['hours'][ $day ] ) ? $d['hours'][ $day ]['open'] . ' – ' . $d['hours'][ $day ]['close'] : __( 'Closed', 'orbitstem-seo' ); $out .= ''; } $out .= '
' . esc_html( $label ) . '' . esc_html( $times ) . '
'; } return $out . '
'; } }