translated string. */ public static function get_labels() { $labels = array( // Section headers. 'section_freelancer' => __( 'Freelancer', 'acknowledgify' ), 'section_team' => __( 'Team', 'acknowledgify' ), 'section_project_credits' => __( 'Project Credits', 'acknowledgify' ), // Freelancer field labels. 'freelancer_name' => __( 'Freelancer', 'acknowledgify' ), 'freelancer_website' => __( 'Website', 'acknowledgify' ), 'freelancer_email' => __( 'Email', 'acknowledgify' ), 'freelancer_location' => __( 'Location', 'acknowledgify' ), 'freelancer_project' => __( 'Project', 'acknowledgify' ), 'freelancer_role' => __( 'Role', 'acknowledgify' ), 'freelancer_thanks_to' => __( 'Thanks To', 'acknowledgify' ), // Agency field labels. 'agency_name' => __( 'Agency', 'acknowledgify' ), 'agency_website' => __( 'Website', 'acknowledgify' ), 'agency_contact' => __( 'Contact', 'acknowledgify' ), 'agency_location' => __( 'Location', 'acknowledgify' ), 'agency_project' => __( 'Project', 'acknowledgify' ), 'agency_thanks' => __( 'Special Thanks', 'acknowledgify' ), ); /** * Filters the labels used in humans.txt output and the admin preview. * * Lets a site change wording without supplying a translation file. * * @param array $labels Label key => string. */ return apply_filters( 'acknowledgify_humans_txt_labels', $labels ); } /** * Suggested role names offered in the Credits repeater. * * Suggestions only. The Role field is free text, so a user is never * limited to this list and no release is needed to add to it. * * @return string[] */ public static function get_role_suggestions() { $roles = array( __( 'Design', 'acknowledgify' ), __( 'Development', 'acknowledgify' ), __( 'SEO & Performance', 'acknowledgify' ), __( 'Hosting & Maintenance', 'acknowledgify' ), __( 'Photography', 'acknowledgify' ), __( 'Videography', 'acknowledgify' ), __( 'Copywriting', 'acknowledgify' ), __( 'Branding', 'acknowledgify' ), __( 'Illustration', 'acknowledgify' ), __( 'Content Strategy', 'acknowledgify' ), __( 'Accessibility', 'acknowledgify' ), __( 'Project Management', 'acknowledgify' ), __( 'Translation', 'acknowledgify' ), __( 'Analytics', 'acknowledgify' ), ); /** * Filters the suggested role names in the Credits repeater. * * @param string[] $roles Suggested role names. */ return (array) apply_filters( 'acknowledgify_role_suggestions', $roles ); } /** * Render the stored credit rows as humans.txt lines. * * A row needs both a role and a name to produce output. A part-filled row * is kept in the database so a half-finished edit is not thrown away on * save, but it prints nothing. * * @return string Zero or more lines, each ending in a newline. */ public static function render_credits() { $output = ''; foreach ( Helper::get_credits() as $credit ) { $role = Helper::plain_text( Tokens::resolve( $credit['role'] ) ); $name = Helper::plain_text( Tokens::resolve( $credit['name'] ) ); $url = Helper::plain_text( Tokens::resolve( $credit['url'] ) ); if ( '' === $role || '' === $name ) { continue; } $line = $role . ': ' . $name; if ( '' !== $url ) { $line .= ' (' . $url . ')'; } $output .= $line . "\n"; } return $output; } /** * Assemble the file body for the saved template. * * Separated from output_humans_txt() so the body can be built without * sending headers, which makes it testable and reusable. * * @return string */ public static function get_body() { $labels = self::get_labels(); $template = (string) Helper::get_setting( 'humans_template', 'freelancer' ); $allowed = Helper::get_allowed_values()['humans_template']; if ( ! in_array( $template, $allowed, true ) ) { $template = 'freelancer'; } $output = self::get_header(); if ( 'custom' === $template ) { // Plain-text response: the author's line breaks are preserved // verbatim. Content is sanitised on input. $output .= Helper::plain_text( Tokens::resolve( (string) Helper::get_setting( 'humans_custom_content', '' ) ), true ); return $output; } if ( 'agency' === $template ) { $identity_label = $labels['section_team']; $identity_keys = array( 'agency_name' => $labels['agency_name'], 'agency_website' => $labels['agency_website'], 'agency_contact' => $labels['agency_contact'], 'agency_location' => $labels['agency_location'], ); $before_credits = array( 'agency_project' => $labels['agency_project'] ); $after_credits = array( 'agency_thanks' => $labels['agency_thanks'] ); } else { $identity_label = $labels['section_freelancer']; $identity_keys = array( 'freelancer_name' => $labels['freelancer_name'], 'freelancer_website' => $labels['freelancer_website'], 'freelancer_email' => $labels['freelancer_email'], 'freelancer_location' => $labels['freelancer_location'], ); $before_credits = array( 'freelancer_project' => $labels['freelancer_project'], 'freelancer_role' => $labels['freelancer_role'], ); $after_credits = array( 'freelancer_thanks_to' => $labels['freelancer_thanks_to'] ); } $identity = ''; foreach ( $identity_keys as $key => $label ) { $identity .= Helper::format_setting( $key, $label ); } if ( '' !== $identity ) { $output .= $identity_label . "\n" . $identity; } $credits = ''; foreach ( $before_credits as $key => $label ) { $credits .= Helper::format_setting( $key, $label ); } // Credit rows sit exactly where the four fixed agency role fields used // to print, so an upgraded site's file is unchanged. $credits .= self::render_credits(); foreach ( $after_credits as $key => $label ) { $credits .= Helper::format_setting( $key, $label ); } if ( '' !== $credits ) { $output .= "\n\n" . $labels['section_project_credits'] . "\n" . $credits; } return $output; } /** * Send the humans.txt response, or a 404 when the feature is off. * * @return void */ public static function output_humans_txt() { if ( ! self::is_humans_txt_request() ) { return; } if ( 1 !== (int) Helper::get_setting( 'enable_humans_txt', 0 ) ) { global $wp_query; if ( $wp_query instanceof \WP_Query ) { $wp_query->set_404(); } status_header( 404 ); nocache_headers(); return; } $output = self::get_body(); status_header( 200 ); send_nosniff_header(); header( 'Content-Type: text/plain; charset=utf-8' ); /** * Filters how long humans.txt may be cached, in seconds. * * The response used to send no-cache headers, which meant every hit * booted WordPress in full. The file changes only when someone edits * the settings, so it is safe to cache. Return 0 to send no-cache * headers instead. * * @param int $max_age Default one hour. */ $max_age = (int) apply_filters( 'acknowledgify_humans_txt_max_age', HOUR_IN_SECONDS ); if ( $max_age > 0 ) { header( 'Cache-Control: public, max-age=' . $max_age ); } else { nocache_headers(); } /* * This response is text/plain, not HTML. Escaping here would send * entities to the reader verbatim: "Smith & Jones" instead of * "Smith & Jones". The safeguards for this context are the * Content-Type header above, nosniff, and the control-character and * line-ending normalisation Helper::plain_text() applies to every * value on the way in. Values are also sanitised when saved. */ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Plain-text response; see comment above. echo $output; exit; } /** * Advertise the file in , per the humanstxt.org convention. * * @return void */ public static function add_humans_txt_link() { if ( 1 !== (int) Helper::get_setting( 'enable_humans_txt', 0 ) ) { return; } printf( '' . "\n", esc_url( home_url( '/humans.txt' ) ) ); } }