mirror of
https://github.com/WordPress/five-for-the-future.git
synced 2025-07-02 01:01:18 +03:00
Merge branch 'production' into fix/9
# Conflicts: # themes/wporg-5ftf/single.php
This commit is contained in:
commit
368d05e74d
|
@ -94,6 +94,12 @@
|
|||
<exclude name="WordPress.PHP.DevelopmentFunctions.error_log_print_r" />
|
||||
</rule>
|
||||
|
||||
<!-- Ignore the CPT template filename, since it is based on the CPT name, and can't change. -->
|
||||
<rule ref="WordPress.Files.FileName.NotHyphenatedLowercase">
|
||||
<exclude-pattern>*/template-parts/content-5ftf_pledge.php$</exclude-pattern>
|
||||
<exclude-pattern>*/archive-5ftf_pledge.php$</exclude-pattern>
|
||||
</rule>
|
||||
|
||||
<rule ref="WordPress-Docs">
|
||||
<!-- If files/variables are given descriptive names like they should be, then an explicit description is usually unnecessary, so leave this as a judgement call. -->
|
||||
<exclude name="Squiz.Commenting.FunctionComment.MissingParamComment" />
|
||||
|
|
|
@ -100,6 +100,11 @@ function populate_list_table_columns( $column, $post_id ) {
|
|||
$contributor = get_post( $post_id );
|
||||
$pledge = get_post( $contributor->post_parent );
|
||||
|
||||
if ( ! $pledge ) {
|
||||
esc_html_e( 'Unattached', 'wordpressorg' );
|
||||
break;
|
||||
}
|
||||
|
||||
$pledge_name = get_the_title( $pledge );
|
||||
|
||||
if ( current_user_can( 'edit_post', $pledge->ID ) ) {
|
||||
|
@ -134,6 +139,20 @@ function create_new_contributor( $wporg_username, $pledge_id ) {
|
|||
return wp_insert_post( $args, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a contributor post from a pledge.
|
||||
*
|
||||
* This wrapper function ensures we have a standardized way of removing a contributor that will still
|
||||
* transition a post status (see PledgeMeta\update_confirmed_contributor_count).
|
||||
*
|
||||
* @param int $contributor_post_id
|
||||
*
|
||||
* @return false|WP_Post|null
|
||||
*/
|
||||
function remove_contributor( $contributor_post_id ) {
|
||||
return wp_trash_post( $contributor_post_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contributor posts associated with a particular pledge post.
|
||||
*
|
||||
|
|
143
plugins/wporg-5ftf/includes/email.php
Normal file
143
plugins/wporg-5ftf/includes/email.php
Normal file
|
@ -0,0 +1,143 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Helper functions for sending emails, including authentication tokens.
|
||||
*
|
||||
* We don't want pledges connected to individual w.org accounts, because that would encourage people to create
|
||||
* "company" accounts instead of having their contributions show up as real human beings; or because that
|
||||
* individual will likely eventually leave the company, and "ownership" of the pledge would be orphaned; or
|
||||
* because we'd have to tie multiple accounts to the pledge (and all the extra time/UX costs associated with that),
|
||||
* and would still have problems with orphaned ownership, etc.
|
||||
*
|
||||
* So instead, we just ask companies to create pledges using a group email (e.g., support@wordcamp.org), and
|
||||
* we email them time-restricted, once-time-use auth tokens when they want to "log in".
|
||||
*
|
||||
* WP "nonces" aren't ideal for this purpose from a security perspective, because they're less secure. They're
|
||||
* reusable, last up to 24 hours, and have a much smaller search space in brute force attacks. They're only
|
||||
* intended to prevent CSRF, and should not be used for authentication or authorization.
|
||||
*
|
||||
* They also create an inconsistent UX, because a nonce could be valid for 24 hours, or for 1 second, due to their
|
||||
* stateless nature -- see `wp_nonce_tick()`. That would lead to some situations where a nonce had already expired
|
||||
* by the time the contributor opened the email and clicked on the link.
|
||||
*
|
||||
* So instead, true stateful CSPRN authentication tokens are generated; see `get_authentication_url()` and
|
||||
* `is_valid_authentication_token()` for details.
|
||||
*
|
||||
* For additional background:
|
||||
* - https://stackoverflow.com/a/35715087/450127 (which is better security advice than ircmarxell's 2010 answer).
|
||||
*/
|
||||
|
||||
namespace WordPressDotOrg\FiveForTheFuture\Email;
|
||||
|
||||
defined( 'WPINC' ) || die();
|
||||
|
||||
const TOKEN_PREFIX = '5ftf_auth_token_';
|
||||
|
||||
// Longer than `get_password_reset_key()` just to be safe. See https://core.trac.wordpress.org/ticket/43546#comment:34
|
||||
const TOKEN_LENGTH = 32;
|
||||
|
||||
/**
|
||||
* Wrap `wp_mail()` with shared functionality.
|
||||
*
|
||||
* @param string $to
|
||||
* @param string $subject
|
||||
* @param string $message
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function send_email( $to, $subject, $message ) {
|
||||
$headers = array(
|
||||
'From: WordPress - Five for the Future <donotreply@wordpress.org>',
|
||||
'Reply-To: support@wordcamp.org',
|
||||
// todo update address when new one is created
|
||||
);
|
||||
|
||||
return wp_mail( $to, $subject, $message, $headers );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an action URL with a secure, unique authentication token.
|
||||
*
|
||||
* @param int $pledge_id
|
||||
* @param string $action
|
||||
* @param int $action_page_id The ID of the page that the user will be taken back to, in order to process their
|
||||
* verification request.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function get_authentication_url( $pledge_id, $action, $action_page_id ) {
|
||||
$auth_token = array(
|
||||
// This will create a CSPRN and is similar to how `get_password_reset_key()` works.
|
||||
'value' => wp_generate_password( TOKEN_LENGTH, false ),
|
||||
// todo should encrypt at rest? core doesn't but others do
|
||||
'expiration' => time() + ( 2 * HOUR_IN_SECONDS ),
|
||||
);
|
||||
|
||||
/*
|
||||
* Tying the token to a specific pledge is important for security, otherwise companies could get a valid token
|
||||
* for their pledge, and use it to edit other company's pledges.
|
||||
*
|
||||
* Similarly, tying it to specific actions is also important, to protect against CSRF attacks.
|
||||
*
|
||||
* This function intentionally requires the caller to pass in a pledge ID and action, so that it can guarantee
|
||||
* that each token will be unique across pledges and actions.
|
||||
*/
|
||||
update_post_meta( $pledge_id, TOKEN_PREFIX . $action, $auth_token );
|
||||
|
||||
$auth_url = add_query_arg(
|
||||
array(
|
||||
'action' => $action,
|
||||
'pledge_id' => $pledge_id,
|
||||
'auth_token' => $auth_token['value'],
|
||||
),
|
||||
get_permalink( $action_page_id )
|
||||
);
|
||||
|
||||
// todo include a "this lnk will expire in 10 hours and after its used once" message too?
|
||||
// probably, but what's the best way to do that DRYly?
|
||||
|
||||
return $auth_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify whether or not a given authentication token is valid.
|
||||
*
|
||||
* These tokens are more secure than WordPress' imitation nonces, because they can only be used once, and expire
|
||||
* in a shorter timeframe. Like WP nonces, though, they must be tied to a specific action and post object in order
|
||||
* to prevent misuse.
|
||||
*
|
||||
* @param $pledge_id
|
||||
* @param $action
|
||||
* @param $unverified_token
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function is_valid_authentication_token( $pledge_id, $action, $unverified_token ) {
|
||||
$verified = false;
|
||||
$valid_token = get_post_meta( $pledge_id, TOKEN_PREFIX . $action, true );
|
||||
|
||||
/*
|
||||
* Later on we'll compare the value to user input, and the user could input null/false/etc, so let's guarantee
|
||||
* that the thing we're comparing against is really what we expect it to be.
|
||||
*/
|
||||
if ( ! is_array( $valid_token ) || ! array_key_exists( 'value', $valid_token ) || ! array_key_exists( 'expiration', $valid_token ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! is_string( $valid_token['value'] ) || TOKEN_LENGTH !== strlen( $valid_token['value'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $valid_token && $valid_token['expiration'] > time() && $unverified_token === $valid_token['value'] ) {
|
||||
$verified = true;
|
||||
|
||||
// Tokens should not be reusable, to increase security.
|
||||
delete_post_meta( $pledge_id, TOKEN_PREFIX . $action );
|
||||
// todo when used to manage pledge, token will probably get deleted when viewing, and then they won't be able to save
|
||||
// fix that when create the manage process, though. for now this works for confirming email address.
|
||||
// maye pass a `context` param to this function, either 'view' or 'update', and only delete if context is 'update' ?
|
||||
// make sure view and update functions checks to make sure have valid token, not create though
|
||||
}
|
||||
|
||||
return $verified;
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
namespace WordPressDotOrg\FiveForTheFuture\PledgeForm;
|
||||
|
||||
use WordPressDotOrg\FiveForTheFuture;
|
||||
use WordPressDotOrg\FiveForTheFuture\{ Pledge, PledgeMeta, Contributor };
|
||||
use WordPressDotOrg\FiveForTheFuture\{ Pledge, PledgeMeta, Contributor, Email };
|
||||
use WP_Error, WP_User;
|
||||
|
||||
defined( 'WPINC' ) || die();
|
||||
|
@ -21,11 +21,12 @@ add_shortcode( '5ftf_pledge_form_manage', __NAMESPACE__ . '\render_form_manage'
|
|||
* @return false|string
|
||||
*/
|
||||
function render_form_new() {
|
||||
$action = filter_input( INPUT_POST, 'action' );
|
||||
$action = isset( $_GET['action'] ) ? filter_input( INPUT_GET, 'action' ) : filter_input( INPUT_POST, 'action' );
|
||||
$data = get_form_submission();
|
||||
$messages = [];
|
||||
$complete = false;
|
||||
$directory_url = get_permalink( get_page_by_path( 'pledges' ) );
|
||||
$view = 'form-pledge-new.php';
|
||||
|
||||
if ( 'Submit Pledge' === $action ) {
|
||||
$processed = process_form_new();
|
||||
|
@ -35,11 +36,16 @@ function render_form_new() {
|
|||
} elseif ( 'success' === $processed ) {
|
||||
$complete = true;
|
||||
}
|
||||
} else if ( 'confirm_pledge_email' === $action ) {
|
||||
$view = 'form-pledge-confirm-email.php';
|
||||
$pledge_id = filter_input( INPUT_GET, 'pledge_id', FILTER_VALIDATE_INT );
|
||||
$unverified_token = filter_input( INPUT_GET, 'auth_token', FILTER_SANITIZE_STRING );
|
||||
$email_confirmed = process_email_confirmation( $pledge_id, $action, $unverified_token );
|
||||
}
|
||||
|
||||
ob_start();
|
||||
$readonly = false;
|
||||
require FiveForTheFuture\PATH . 'views/form-pledge-new.php';
|
||||
require FiveForTheFuture\get_views_path() . $view;
|
||||
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
@ -111,6 +117,40 @@ function process_form_new() {
|
|||
return 'success';
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a request to confirm a company's email address.
|
||||
*
|
||||
* @param int $pledge_id
|
||||
* @param string $action
|
||||
* @param array $unverified_token
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function process_email_confirmation( $pledge_id, $action, $unverified_token ) {
|
||||
$meta_key = PledgeMeta\META_PREFIX . 'pledge-email-confirmed';
|
||||
$already_confirmed = get_post( $pledge_id )->$meta_key;
|
||||
|
||||
if ( $already_confirmed ) {
|
||||
/*
|
||||
* If they refresh the page after confirming, they'd otherwise get an error because the token had been
|
||||
* used, and might be confused and think that the address wasn't confirmed.
|
||||
*
|
||||
* This leaks the fact that the address is confirmed, because it will return true even if the token is
|
||||
* invalid, but there aren't any security/privacy implications of that.
|
||||
*/
|
||||
return true;
|
||||
}
|
||||
|
||||
$email_confirmed = Email\is_valid_authentication_token( $pledge_id, $action, $unverified_token );
|
||||
|
||||
if ( $email_confirmed ) {
|
||||
update_post_meta( $pledge_id, $meta_key, true );
|
||||
wp_update_post( array( 'ID' => $pledge_id, 'post_status' => 'publish' ) );
|
||||
}
|
||||
|
||||
return $email_confirmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the form(s) for managing existing pledges.
|
||||
*
|
||||
|
@ -179,6 +219,12 @@ function process_form_manage() {
|
|||
__( 'A pledge already exists for this domain.', 'wporg' )
|
||||
);
|
||||
}
|
||||
|
||||
// todo email any new contributors for confirmation
|
||||
// notify any removed contributors?
|
||||
// ask them to update their profiles?
|
||||
// automatically update contributor profiles?
|
||||
// anything else?
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -15,10 +15,11 @@ defined( 'WPINC' ) || die();
|
|||
|
||||
const META_PREFIX = FiveForTheFuture\PREFIX . '_';
|
||||
|
||||
add_action( 'init', __NAMESPACE__ . '\register_pledge_meta' );
|
||||
add_action( 'admin_init', __NAMESPACE__ . '\add_meta_boxes' );
|
||||
add_action( 'save_post', __NAMESPACE__ . '\save_pledge', 10, 2 );
|
||||
add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\enqueue_assets' );
|
||||
add_action( 'init', __NAMESPACE__ . '\register_pledge_meta' );
|
||||
add_action( 'admin_init', __NAMESPACE__ . '\add_meta_boxes' );
|
||||
add_action( 'save_post', __NAMESPACE__ . '\save_pledge', 10, 2 );
|
||||
add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\enqueue_assets' );
|
||||
add_action( 'transition_post_status', __NAMESPACE__ . '\update_confirmed_contributor_count', 10, 3 );
|
||||
|
||||
// Both hooks must be used because `updated` doesn't fire if the post meta didn't previously exist.
|
||||
add_action( 'updated_postmeta', __NAMESPACE__ . '\update_generated_meta', 10, 4 );
|
||||
|
@ -31,49 +32,48 @@ add_action( 'added_post_meta', __NAMESPACE__ . '\update_generated_meta', 10, 4
|
|||
*/
|
||||
function get_pledge_meta_config( $context = '' ) {
|
||||
$user_input = array(
|
||||
'org-description' => array(
|
||||
'org-description' => array(
|
||||
'single' => true,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
'show_in_rest' => true,
|
||||
'php_filter' => FILTER_SANITIZE_STRING,
|
||||
),
|
||||
'org-name' => array(
|
||||
'org-name' => array(
|
||||
'single' => true,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
'show_in_rest' => true,
|
||||
'php_filter' => FILTER_SANITIZE_STRING,
|
||||
),
|
||||
'org-url' => array(
|
||||
'org-url' => array(
|
||||
'single' => true,
|
||||
'sanitize_callback' => 'esc_url_raw',
|
||||
'show_in_rest' => true,
|
||||
'php_filter' => FILTER_VALIDATE_URL,
|
||||
),
|
||||
'org-pledge-email' => array(
|
||||
'org-pledge-email' => array(
|
||||
'single' => true,
|
||||
'sanitize_callback' => 'sanitize_email',
|
||||
'show_in_rest' => false,
|
||||
'php_filter' => FILTER_VALIDATE_EMAIL,
|
||||
),
|
||||
'org-number-employees' => array(
|
||||
'single' => true,
|
||||
'sanitize_callback' => 'absint',
|
||||
'show_in_rest' => false,
|
||||
'php_filter' => FILTER_VALIDATE_INT,
|
||||
),
|
||||
);
|
||||
|
||||
$generated = array(
|
||||
'org-domain' => array(
|
||||
'org-domain' => array(
|
||||
'single' => true,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
'show_in_rest' => false,
|
||||
),
|
||||
'pledge-email-confirmed' => array(
|
||||
'pledge-email-confirmed' => array(
|
||||
'single' => true,
|
||||
'sanitize_callback' => 'wp_validate_boolean',
|
||||
'show_in_rest' => false,
|
||||
),
|
||||
'pledge-confirmed-contributors' => array(
|
||||
'single' => true,
|
||||
'sanitize_callback' => 'absint',
|
||||
'show_in_rest' => false,
|
||||
),
|
||||
);
|
||||
|
||||
switch ( $context ) {
|
||||
|
@ -184,10 +184,19 @@ function render_meta_boxes( $pledge, $box ) {
|
|||
* @param WP_Post $pledge
|
||||
*/
|
||||
function save_pledge( $pledge_id, $pledge ) {
|
||||
$action = filter_input( INPUT_GET, 'action' );
|
||||
$get_action = filter_input( INPUT_GET, 'action' );
|
||||
$post_action = filter_input( INPUT_POST, 'action' );
|
||||
$ignored_actions = array( 'trash', 'untrash', 'restore' );
|
||||
|
||||
if ( $action && in_array( $action, $ignored_actions, true ) ) {
|
||||
/*
|
||||
* This is only intended to run when the front end form and wp-admin forms are submitted, not when posts are
|
||||
* programmatically updated.
|
||||
*/
|
||||
if ( 'Submit Pledge' !== $post_action && 'editpost' !== $get_action ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $get_action && in_array( $get_action, $ignored_actions, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -276,6 +285,36 @@ function update_generated_meta( $meta_id, $object_id, $meta_key, $_meta_value )
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the cached count of confirmed contributors for a pledge when a contributor post changes statuses.
|
||||
*
|
||||
* Note that contributor posts should always be trashed instead of deleted completely when a contributor is
|
||||
* removed from a pledge.
|
||||
*
|
||||
* @param string $new_status
|
||||
* @param string $old_status
|
||||
* @param WP_Post $post
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function update_confirmed_contributor_count( $new_status, $old_status, WP_Post $post ) {
|
||||
if ( Contributor\CPT_ID !== get_post_type( $post ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $new_status === $old_status ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pledge = get_post( $post->post_parent );
|
||||
|
||||
if ( $pledge instanceof WP_Post ) {
|
||||
$confirmed_contributors = Contributor\get_pledge_contributors( $pledge->ID, 'publish' );
|
||||
|
||||
update_post_meta( $pledge->ID, META_PREFIX . 'pledge-confirmed-contributors', count( $confirmed_contributors ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that an array contains values for all required keys.
|
||||
*
|
||||
|
|
|
@ -5,9 +5,11 @@
|
|||
*/
|
||||
|
||||
namespace WordPressDotOrg\FiveForTheFuture\Pledge;
|
||||
use WordPressDotOrg\FiveForTheFuture\Email;
|
||||
|
||||
use WordPressDotOrg\FiveForTheFuture;
|
||||
use WP_Error;
|
||||
use const WordPressDotOrg\FiveForTheFuture\PledgeMeta\META_PREFIX;
|
||||
|
||||
defined( 'WPINC' ) || die();
|
||||
|
||||
|
@ -17,6 +19,7 @@ const CPT_ID = FiveForTheFuture\PREFIX . '_' . SLUG;
|
|||
|
||||
add_action( 'init', __NAMESPACE__ . '\register', 0 );
|
||||
add_action( 'admin_menu', __NAMESPACE__ . '\admin_menu' );
|
||||
add_action( 'pre_get_posts', __NAMESPACE__ . '\filter_query' );
|
||||
|
||||
/**
|
||||
* Register all the things.
|
||||
|
@ -127,5 +130,92 @@ function create_new_pledge( $name ) {
|
|||
'post_status' => 'draft',
|
||||
);
|
||||
|
||||
return wp_insert_post( $args, true );
|
||||
|
||||
$pledge_id = wp_insert_post( $args, true );
|
||||
// The pledge's meta data is saved at this point via `save_pledge_meta()`, which is a `save_post` callback.
|
||||
|
||||
if ( ! is_wp_error( $pledge_id ) ) {
|
||||
send_pledge_verification_email( $pledge_id, get_post()->ID );
|
||||
}
|
||||
|
||||
return $pledge_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Email pledge manager to confirm their email address.
|
||||
*
|
||||
* @param int $pledge_id The ID of the pledge.
|
||||
* @param int $action_page_id The ID of the page that the user will be taken back to, in order to process their
|
||||
* verification request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function send_pledge_verification_email( $pledge_id, $action_page_id ) {
|
||||
$pledge = get_post( $pledge_id );
|
||||
|
||||
$message =
|
||||
'Thanks for committing to help keep WordPress sustainable! Please confirm this email address ' .
|
||||
'so that we can accept your pledge:' . "\n\n" .
|
||||
Email\get_authentication_url( $pledge_id, 'confirm_pledge_email', $action_page_id )
|
||||
;
|
||||
|
||||
// todo include a notice that the link will expire in X hours, so they know what to expect
|
||||
// need to make that value DRY across all emails with links
|
||||
// should probably say that on the front end form success message as well, so they know to go check their email now instead of after lunch.
|
||||
|
||||
return Email\send_email(
|
||||
$pledge->{'5ftf_org-pledge-email'},
|
||||
'Please confirm your email address',
|
||||
$message
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter query for archive & search pages to ensure we're only showing the expected data.
|
||||
*
|
||||
* @param WP_Query $query The WP_Query instance (passed by reference).
|
||||
* @return void
|
||||
*/
|
||||
function filter_query( $query ) {
|
||||
if ( is_admin() || ! $query->is_main_query() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$contributor_count_key = META_PREFIX . 'pledge-confirmed-contributors';
|
||||
|
||||
// Set up meta queries to include the "valid pledge" check, added to both search and any pledge requests.
|
||||
$meta_queries = (array) $query->get( 'meta_query' );
|
||||
$meta_queries[] = array(
|
||||
'key' => $contributor_count_key,
|
||||
'value' => 0,
|
||||
'compare' => '>',
|
||||
'type' => 'NUMERIC',
|
||||
);
|
||||
|
||||
if ( CPT_ID === $query->get( 'post_type' ) ) {
|
||||
$query->set( 'meta_query', $meta_queries );
|
||||
}
|
||||
|
||||
// Searching is restricted to pledges only.
|
||||
if ( $query->is_search ) {
|
||||
$query->set( 'post_type', CPT_ID );
|
||||
$query->set( 'meta_query', $meta_queries );
|
||||
}
|
||||
|
||||
// Use the custom order param to sort the archive page.
|
||||
if ( $query->is_archive && CPT_ID === $query->get( 'post_type' ) ) {
|
||||
$order = isset( $_GET['order'] ) ? $_GET['order'] : '';
|
||||
switch ( $order ) {
|
||||
case 'alphabetical':
|
||||
$query->set( 'orderby', 'name' );
|
||||
$query->set( 'order', 'ASC' );
|
||||
break;
|
||||
|
||||
case 'contributors':
|
||||
$query->set( 'meta_key', $contributor_count_key );
|
||||
$query->set( 'orderby', 'meta_value_num' );
|
||||
$query->set( 'order', 'DESC' );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ add_action( 'plugins_loaded', __NAMESPACE__ . '\load' );
|
|||
*/
|
||||
function load() {
|
||||
require_once get_includes_path() . 'contributor.php';
|
||||
require_once get_includes_path() . 'email.php';
|
||||
require_once get_includes_path() . 'pledge.php';
|
||||
require_once get_includes_path() . 'pledge-meta.php';
|
||||
require_once get_includes_path() . 'pledge-form.php';
|
||||
|
|
207
plugins/wporg-5ftf/tests/test-email.php
Normal file
207
plugins/wporg-5ftf/tests/test-email.php
Normal file
|
@ -0,0 +1,207 @@
|
|||
<?php
|
||||
|
||||
use function WordPressDotOrg\FiveForTheFuture\Email\{ get_authentication_url, is_valid_authentication_token };
|
||||
use const WordPressDotOrg\FiveForTheFuture\Email\{ TOKEN_PREFIX };
|
||||
use const WordPressDotOrg\FiveForTheFuture\Pledge\CPT_ID as PLEDGE_POST_TYPE;
|
||||
|
||||
defined( 'WPINC' ) || die();
|
||||
|
||||
class Test_Email extends WP_UnitTestCase {
|
||||
protected static $valid_pledge, $valid_action, $valid_action_page, $valid_action_url, $valid_token;
|
||||
|
||||
/**
|
||||
* Setup fixtures that are shared across all tests.
|
||||
*/
|
||||
public static function wpSetUpBeforeClass() {
|
||||
$valid_pledge_params = array(
|
||||
'post_type' => PLEDGE_POST_TYPE,
|
||||
'post_title' => 'Valid Pledge',
|
||||
'post_status' => 'publish',
|
||||
);
|
||||
|
||||
$valid_action_page_params = array(
|
||||
'post_type' => 'page',
|
||||
'post_title' => 'For Organizers',
|
||||
'post_status' => 'publish',
|
||||
);
|
||||
|
||||
$valid_pledge_id = self::factory()->post->create( $valid_pledge_params );
|
||||
|
||||
self::$valid_pledge = get_post( $valid_pledge_id );
|
||||
|
||||
|
||||
$valid_action_page_id = self::factory()->post->create( $valid_action_page_params );
|
||||
self::$valid_action_page = get_post( $valid_action_page_id );
|
||||
|
||||
self::$valid_action = 'confirm_pledge_email';
|
||||
// todo better example action to use, like contributor verifying participation? or is this one just as good?
|
||||
// should probably use the manage one once that's implemented, b/c should test the `view` context and the `update` context.
|
||||
// use this one for now, though.
|
||||
|
||||
self::verify_before_class_fixtures();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify whether or not the fixtures were setup correctly.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected static function verify_before_class_fixtures() {
|
||||
self::assertSame( 'object', gettype( self::$valid_action_page ) );
|
||||
self::assertSame( 'For Organizers', self::$valid_action_page->post_title );
|
||||
self::assertSame( 'object', gettype( self::$valid_pledge ) );
|
||||
self::assertSame( 'Valid Pledge', self::$valid_pledge->post_title );
|
||||
self::assertSame( PLEDGE_POST_TYPE, self::$valid_pledge->post_type );
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup fixtures that are unique for each test.
|
||||
*/
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
/*
|
||||
* `get_authentication_url()` should create a valid token in the database.
|
||||
*
|
||||
* This must be called before every test, because the process of verifying a valid token will delete it.
|
||||
*/
|
||||
self::$valid_action_url = get_authentication_url( self::$valid_pledge->ID, self::$valid_action, self::$valid_action_page->ID );
|
||||
self::$valid_token = get_post_meta( self::$valid_pledge->ID, TOKEN_PREFIX . self::$valid_action, true );
|
||||
|
||||
// Verify that the fixtures are setup correctly.
|
||||
$action_url_args = wp_parse_args( wp_parse_url( self::$valid_action_url, PHP_URL_QUERY ) );
|
||||
|
||||
$this->assertSame( 'array', gettype( self::$valid_token ) );
|
||||
$this->assertSame( $action_url_args['action'], self::$valid_action );
|
||||
$this->assertSame( $action_url_args['auth_token'], self::$valid_token['value'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::is_valid_authentication_token
|
||||
*/
|
||||
public function test_valid_token_accepted() {
|
||||
$verified = is_valid_authentication_token( self::$valid_pledge->ID, self::$valid_action, self::$valid_token['value'] );
|
||||
|
||||
$this->assertTrue( $verified );
|
||||
|
||||
// todo test that `view` and `update` contexts work as well, when those are added
|
||||
// maybe need to test some failures for that too
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::is_valid_authentication_token
|
||||
*
|
||||
* @dataProvider data_invalid_token_rejected
|
||||
*/
|
||||
public function test_invalid_token_rejected( $invalid_token ) {
|
||||
/*
|
||||
* It's expected that some of the values passed in won't have a `value` item, so fallback to the item
|
||||
* itself in those cases, to avoid PHPUnit throwing an exception.
|
||||
*/
|
||||
$invalid_token_value = $invalid_token['value'] ?? $invalid_token;
|
||||
$verified = is_valid_authentication_token( self::$valid_pledge->ID, self::$valid_action, $invalid_token_value );
|
||||
|
||||
$this->assertSame( false, $verified );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that invalid tokens are rejected.
|
||||
*
|
||||
* Note that data providers can't access fixtures.
|
||||
* See https://phpunit.readthedocs.io/en/7.4/writing-tests-for-phpunit.html#data-providers.
|
||||
*
|
||||
* @covers ::is_valid_authentication_token
|
||||
*/
|
||||
public function data_invalid_token_rejected() {
|
||||
return array(
|
||||
'non-existent-token' => array( false ), // Simulates `get_post_meta()` return value.
|
||||
'wrong-data-type' => array( 'this string is not an array' ),
|
||||
'wrong-array-items' => array( 'this' => "doesn't have `value` and `expiration` items" ),
|
||||
|
||||
'invalid-value' => array(
|
||||
array(
|
||||
'value' => 'Valid tokens will never contain special characters like !@#$%^&*()',
|
||||
'expiration' => time() + HOUR_IN_SECONDS,
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::is_valid_authentication_token
|
||||
*/
|
||||
public function test_expired_token_rejected() {
|
||||
$expired_token = self::$valid_token;
|
||||
$expired_token['expiration'] = time() - 1;
|
||||
|
||||
update_post_meta( self::$valid_pledge->ID, TOKEN_PREFIX . self::$valid_action, $expired_token );
|
||||
|
||||
$verified = is_valid_authentication_token( self::$valid_pledge->ID, self::$valid_action, self::$valid_token['value'] );
|
||||
|
||||
$this->assertSame( false, $verified );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::is_valid_authentication_token
|
||||
*/
|
||||
public function test_used_token_rejected() {
|
||||
// The token should be deleted once it's used/verified for the first time.
|
||||
$first_verification = is_valid_authentication_token( self::$valid_pledge->ID, self::$valid_action, self::$valid_token['value'] );
|
||||
$second_verification = is_valid_authentication_token( self::$valid_pledge->ID, self::$valid_action, self::$valid_token['value'] );
|
||||
|
||||
$this->assertSame( true, $first_verification );
|
||||
$this->assertSame( false, $second_verification );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::is_valid_authentication_token
|
||||
*/
|
||||
public function test_valid_token_rejected_for_other_pages() {
|
||||
$verified = is_valid_authentication_token( self::$valid_action_page->ID, self::$valid_action, self::$valid_token['value'] );
|
||||
|
||||
$this->assertSame( false, $verified );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::is_valid_authentication_token
|
||||
*/
|
||||
public function test_valid_token_rejected_for_other_actions() {
|
||||
// Setup another valid token for the other action.
|
||||
$other_valid_action = 'confirm_contributor_participation';
|
||||
// todo update this when the action for that step is created, so that they match and show that valid actions
|
||||
$other_valid_action_url = get_authentication_url( self::$valid_pledge->ID, $other_valid_action, self::$valid_action_page->ID );
|
||||
|
||||
// Intentionally mismatch the token and action.
|
||||
$verified = is_valid_authentication_token( self::$valid_pledge->ID, $other_valid_action, self::$valid_token['value'] );
|
||||
|
||||
$this->assertSame( false, $verified );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::is_valid_authentication_token
|
||||
*/
|
||||
public function test_valid_token_rejected_for_other_pledge() {
|
||||
$other_valid_pledge_params = array(
|
||||
'post_type' => PLEDGE_POST_TYPE,
|
||||
'post_title' => 'Other Valid Pledge',
|
||||
'post_status' => 'publish',
|
||||
);
|
||||
|
||||
$other_valid_pledge_id = self::factory()->post->create( $other_valid_pledge_params );
|
||||
$other_valid_pledge = get_post( $other_valid_pledge_id );
|
||||
|
||||
// Create a valid token for the other pledge.
|
||||
get_authentication_url( $other_valid_pledge->ID, self::$valid_action, self::$valid_action_page->ID );
|
||||
|
||||
$other_valid_token = get_post_meta( $other_valid_pledge->ID, TOKEN_PREFIX . self::$valid_action, true );
|
||||
|
||||
// Intentionally mismatch the pledge and token.
|
||||
$verified = is_valid_authentication_token( $other_valid_pledge_id, self::$valid_action, self::$valid_token['value'] );
|
||||
|
||||
$this->assertSame( 'Other Valid Pledge', $other_valid_pledge->post_title );
|
||||
$this->assertSame( 'array', gettype( $other_valid_token ) );
|
||||
$this->assertArrayHasKey( 'value', $other_valid_token );
|
||||
$this->assertNotSame( $other_valid_token['value'], self::$valid_token['value'] );
|
||||
$this->assertSame( false, $verified );
|
||||
}
|
||||
}
|
42
plugins/wporg-5ftf/views/form-pledge-confirm-email.php
Executable file
42
plugins/wporg-5ftf/views/form-pledge-confirm-email.php
Executable file
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace WordPressDotOrg\FiveForTheFuture\View;
|
||||
|
||||
/**
|
||||
* @var bool $email_confirmed
|
||||
* @var string $directory_url
|
||||
*/
|
||||
|
||||
?>
|
||||
|
||||
<?php if ( true === $email_confirmed ) : ?>
|
||||
|
||||
<div class="notice notice-success notice-alt">
|
||||
<p>
|
||||
Thank you for confirming your address! We've emailed confirmation links to your contributors, and your pledge will show up in <a href="<?php echo esc_url( $directory_url ); ?>">the directory</a> once one of them confirms their participation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<?php else : ?>
|
||||
|
||||
<div class="notice notice-error notice-alt">
|
||||
<p>
|
||||
<?php
|
||||
/*
|
||||
* There could be other reasons it failed, like an invalid token, but this is the most common reason,
|
||||
* and the only one that normal users should experience, so we're assuming it in order to provide
|
||||
* the best UX.
|
||||
*/
|
||||
?>
|
||||
Your confirmation link has expired, please obtain a new one:
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<button class="button-secondary">
|
||||
<?php esc_html_e( 'Resend confirmation email', 'wporg' ); ?>
|
||||
<?php // todo make ^ work when making the other 2 work ?>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
|
@ -11,9 +11,11 @@ use function WordPressDotOrg\FiveForTheFuture\get_views_path;
|
|||
|
||||
?>
|
||||
|
||||
<!-- TODO Reveal this once managing an existing pledge is actually possible.
|
||||
<p>
|
||||
<a href="#">Manage an existing pledge</a>
|
||||
</p>
|
||||
-->
|
||||
|
||||
<?php if ( ! empty( $messages ) ) : ?>
|
||||
|
||||
|
@ -28,7 +30,7 @@ use function WordPressDotOrg\FiveForTheFuture\get_views_path;
|
|||
<?php if ( true === $complete ) : ?>
|
||||
|
||||
<div class="notice notice-success notice-alt">
|
||||
<p><?php esc_html_e( 'Thanks for pledging to Five for the Future! Your new pledge profile has been created, and we’ve emailed you a link to confirm your address. Your contributors have also been emailed a link to confirm their participation with your organization.', 'wporg' ); ?></p>
|
||||
<p><?php esc_html_e( "Thanks for pledging to Five for the Future! Your new pledge profile has been created, and we’ve emailed you a link to confirm your address. Once that's done, we'll also email confirmation links to your contributors.", 'wporg' ); ?></p>
|
||||
|
||||
<p>
|
||||
<?php echo wp_kses_post( sprintf(
|
||||
|
@ -40,6 +42,9 @@ use function WordPressDotOrg\FiveForTheFuture\get_views_path;
|
|||
<p>
|
||||
<?php echo wp_kses_post(
|
||||
__( 'Do you want to hire additional employees to contribute to WordPress? <a href="https://jobs.wordpress.net">Post a job listing on jobs.wordpress.net</a>.', 'wporg' )
|
||||
// todo ask mel about moving this outside the `notice-success`, since it's not really part of the success notification, and distracts from it.
|
||||
// many users have notification fatigue and no longer trust them or pay attention to them, because they're so often misused for non-critical information,
|
||||
// and the jobs thing is more of an "ad" in this context than something directly related to the process the user wants to complete
|
||||
); ?>
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
@ -7,7 +7,7 @@ namespace WordPressDotOrg\FiveForTheFuture\View;
|
|||
|
||||
<div class="form-field">
|
||||
<label for="5ftf-pledge-contributors">
|
||||
<?php esc_html_e( 'Contributors', 'wordpressorg' ); ?>
|
||||
<?php esc_html_e( 'Contributor Usernames', 'wordpressorg' ); ?>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
|
@ -18,6 +18,6 @@ namespace WordPressDotOrg\FiveForTheFuture\View;
|
|||
aria-describedby="5ftf-pledge-contributors-help"
|
||||
/>
|
||||
<p id="5ftf-pledge-contributors-help">
|
||||
<?php esc_html_e( 'Separate each username with a comma.', 'wordpressorg' ); ?>
|
||||
<?php esc_html_e( 'Separate each WordPress.org username with a comma.', 'wordpressorg' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
@ -60,17 +60,3 @@ namespace WordPressDotOrg\FiveForTheFuture\View;
|
|||
echo esc_html( $data['org-description'] );
|
||||
/* phpcs:ignore */ ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="5ftf-org-number-employees">
|
||||
<?php esc_html_e( 'Number of Employees Being Contributed', 'wordpressorg' ); ?>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="5ftf-org-number-employees"
|
||||
name="org-number-employees"
|
||||
value="<?php echo esc_attr( $data['org-number-employees'] ); ?>"
|
||||
required
|
||||
<?php echo $readonly ? 'readonly' : ''; ?>
|
||||
/>
|
||||
</div>
|
||||
|
|
72
themes/wporg-5ftf/archive-5ftf_pledge.php
Normal file
72
themes/wporg-5ftf/archive-5ftf_pledge.php
Normal file
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
namespace WordPressdotorg\Five_for_the_Future\Theme;
|
||||
|
||||
use const WordPressDotOrg\FiveForTheFuture\Pledge\CPT_ID;
|
||||
|
||||
// If we don't have any posts to display for the archive, then send a 404 status. See #meta4151.
|
||||
if ( ! have_posts() ) {
|
||||
status_header( 404 );
|
||||
nocache_headers();
|
||||
}
|
||||
|
||||
get_header(); ?>
|
||||
|
||||
<main id="main" class="site-main" role="main">
|
||||
|
||||
<?php if ( have_posts() ) : ?>
|
||||
|
||||
<header class="page-header">
|
||||
<h1 class="page-title"><?php esc_html_e( 'Pledges', 'wordpressorg' ); ?></h1>
|
||||
<div class="page-header-callout">
|
||||
<a class="button" href="/for-organizations/" >
|
||||
<?php esc_html_e( 'Pledge your company', 'wordpressorg' ); ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="page-header-controls">
|
||||
<form method="get" action="<?php echo esc_url( get_post_type_archive_link( CPT_ID ) ); ?>">
|
||||
<label for="pledge-sort"><?php esc_html_e( 'Sort pledges by', 'wordpressorg' ); ?></label>
|
||||
<select class="custom-select" id="pledge-sort" name="order">
|
||||
<option value="" <?php selected( $_GET['order'], '' ); ?>>
|
||||
<?php esc_html_e( 'All Pledges', 'wordpressorg' ); ?>
|
||||
</option>
|
||||
<option value="alphabetical" <?php selected( $_GET['order'], 'alphabetical' ); ?>>
|
||||
<?php esc_html_e( 'Alphabetical', 'wordpressorg' ); ?>
|
||||
</option>
|
||||
<option value="contributors" <?php selected( $_GET['order'], 'contributors' ); ?>>
|
||||
<?php esc_html_e( 'Total Contributors', 'wordpressorg' ); ?>
|
||||
</option>
|
||||
</select>
|
||||
<span class="screen-reader-text">
|
||||
<input type="submit" />
|
||||
</span>
|
||||
</form>
|
||||
|
||||
<?php get_search_form(); ?>
|
||||
</div>
|
||||
</header><!-- .page-header -->
|
||||
|
||||
<?php
|
||||
|
||||
while ( have_posts() ) :
|
||||
the_post();
|
||||
|
||||
get_template_part( 'template-parts/content', get_post_type() );
|
||||
|
||||
endwhile;
|
||||
|
||||
the_posts_pagination();
|
||||
|
||||
?>
|
||||
|
||||
<?php else :
|
||||
|
||||
get_template_part( 'template-parts/content', 'none' );
|
||||
|
||||
endif; ?>
|
||||
|
||||
</main><!-- #main -->
|
||||
|
||||
<?php
|
||||
get_footer();
|
|
@ -26,23 +26,19 @@ get_header(); ?>
|
|||
while ( have_posts() ) :
|
||||
the_post();
|
||||
|
||||
/*
|
||||
* Include the Post-Format-specific template for the content.
|
||||
* If you want to override this in a child theme, then include a file
|
||||
* called content-___.php (where ___ is the Post Format name) and that will be used instead.
|
||||
*/
|
||||
get_template_part( 'template-parts/content', 'page' );
|
||||
get_template_part( 'template-parts/content', get_post_type() );
|
||||
|
||||
endwhile;
|
||||
|
||||
the_posts_pagination();
|
||||
|
||||
else :
|
||||
?>
|
||||
|
||||
<?php else :
|
||||
|
||||
get_template_part( 'template-parts/content', 'none' );
|
||||
|
||||
endif;
|
||||
?>
|
||||
endif; ?>
|
||||
|
||||
</main><!-- #main -->
|
||||
|
||||
|
|
|
@ -5,3 +5,4 @@
|
|||
@import "../../../pub/wporg/css/base/lists";
|
||||
@import "../../../pub/wporg/css/base/tables";
|
||||
@import "../../../pub/wporg/css/base/typography";
|
||||
@import "select";
|
||||
|
|
40
themes/wporg-5ftf/css/base/_select.scss
Normal file
40
themes/wporg-5ftf/css/base/_select.scss
Normal file
|
@ -0,0 +1,40 @@
|
|||
// Remove the browser styling from a select box and create a simple dropdown.
|
||||
// See https://www.filamentgroup.com/lab/select-css.html
|
||||
.custom-select {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
margin: -0.5rem 0;
|
||||
padding: 0.5rem 2rem 0.5rem .8rem;
|
||||
width: auto;
|
||||
|
||||
font-size: 1em;
|
||||
line-height: 1.3;
|
||||
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
border-radius: 0.5em;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
|
||||
background-color: transparent;
|
||||
background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg width="14" height="8" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M2 0L7 5L12 0L14 1L7 8L0 1L2 0Z" fill="%23555D66"/%3E%3C/svg%3E%0A');
|
||||
background-repeat: no-repeat;
|
||||
background-position: right .7em top 50%;
|
||||
background-size: .65em auto;
|
||||
|
||||
&::-ms-expand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 1px 3px rgba(59, 153, 252, .7);
|
||||
box-shadow: 0 0 0 3px -moz-mac-focusring;
|
||||
color: #222;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
& option {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
9
themes/wporg-5ftf/css/components/_archive.scss
Normal file
9
themes/wporg-5ftf/css/components/_archive.scss
Normal file
|
@ -0,0 +1,9 @@
|
|||
body.archive {
|
||||
|
||||
.page-title {
|
||||
color: $color__text-heading-darker;
|
||||
font-size: ms(8);
|
||||
font-weight: 300;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
|
@ -20,8 +20,10 @@
|
|||
@import "../../../pub/wporg/css/components/wporg-footer";
|
||||
@import "../../../pub/wporg/css/components/wporg-header";
|
||||
@import "about";
|
||||
@import "archive";
|
||||
@import "entry-content";
|
||||
@import "page";
|
||||
@import "pledge-list";
|
||||
@import "site-content";
|
||||
@import "site-header";
|
||||
@import "site-title";
|
||||
|
|
58
themes/wporg-5ftf/css/components/_pledge-list.scss
Normal file
58
themes/wporg-5ftf/css/components/_pledge-list.scss
Normal file
|
@ -0,0 +1,58 @@
|
|||
body.archive.post-type-archive-5ftf_pledge {
|
||||
|
||||
// Expand archive content area to full-width of header.
|
||||
.site-content .site-main {
|
||||
padding: 0 10px;
|
||||
max-width: $size__site-main;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
margin: ms(12) 0;
|
||||
|
||||
.page-title {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.page-header-callout {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.page-header-controls {
|
||||
grid-column: 1 / span 2;
|
||||
grid-row: 2;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
margin-top: ms(2);
|
||||
border-top: 1px solid $color-gray-light-500;
|
||||
padding-top: ms(2);
|
||||
|
||||
form:nth-of-type(2n+1) {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
form:nth-of-type(2n) {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-header-controls {
|
||||
font-size: ms(-2);
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
font-size: 1em;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,4 +10,5 @@
|
|||
@import "hero";
|
||||
@import "image";
|
||||
@import "pledge-form";
|
||||
@import "pledge";
|
||||
@import "pullquote";
|
||||
|
|
74
themes/wporg-5ftf/css/objects/_pledge.scss
Normal file
74
themes/wporg-5ftf/css/objects/_pledge.scss
Normal file
|
@ -0,0 +1,74 @@
|
|||
article.type-5ftf_pledge {
|
||||
/* Structure */
|
||||
display: grid;
|
||||
grid-template-columns: 330px auto;
|
||||
margin-bottom: ms(12);
|
||||
|
||||
.entry-image {
|
||||
grid-column: 1;
|
||||
grid-row: 1 / span 2;
|
||||
margin-right: ms(8);
|
||||
}
|
||||
|
||||
.entry-header {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.entry-content {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
/* Styles */
|
||||
|
||||
.entry-image__placeholder {
|
||||
background: $color-gray-light-100;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.entry-image__logo {
|
||||
padding: ms(-2);
|
||||
background: $color-gray-light-100;
|
||||
text-align: center;
|
||||
|
||||
img {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
max-height: 100px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.entry-title {
|
||||
margin-top: 0;
|
||||
font-size: ms(2);
|
||||
font-weight: 400;
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.entry-content {
|
||||
font-size: ms(-1);
|
||||
color: $color__text-darker;
|
||||
}
|
||||
|
||||
.pledge-contributors h3 {
|
||||
margin-top: 0;
|
||||
font-size: ms(-1);
|
||||
color: $color__text-lighter;
|
||||
}
|
||||
|
||||
.pledge-contributor__avatar {
|
||||
display: inline-block;
|
||||
background: $color-gray-light-700;
|
||||
|
||||
img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,6 @@
|
|||
<?php
|
||||
namespace WordPressDotOrg\FiveForTheFuture\Theme;
|
||||
|
||||
$post_type = get_post_type(); // 5ftf_pledge
|
||||
|
||||
get_header(); ?>
|
||||
|
||||
<main id="main" class="site-main" role="main">
|
||||
|
@ -10,7 +8,8 @@ get_header(); ?>
|
|||
<?php while ( have_posts() ) :
|
||||
the_post();
|
||||
|
||||
get_template_part( 'template-parts/' . $post_type, 'single' );
|
||||
get_template_part( 'template-parts/content', get_post_type() );
|
||||
|
||||
endwhile; ?>
|
||||
|
||||
</main><!-- #main -->
|
||||
|
|
77
themes/wporg-5ftf/template-parts/content-5ftf_pledge.php
Normal file
77
themes/wporg-5ftf/template-parts/content-5ftf_pledge.php
Normal file
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
/**
|
||||
* Display pledge data in the archive & single view.
|
||||
*/
|
||||
|
||||
namespace WordPressdotorg\Five_for_the_Future\Theme;
|
||||
|
||||
use WordPressDotOrg\FiveForTheFuture\Contributor;
|
||||
use WordPressDotOrg\FiveForTheFuture\PledgeMeta;
|
||||
|
||||
$data = array();
|
||||
|
||||
foreach ( PledgeMeta\get_pledge_meta_config() as $key => $config ) {
|
||||
$data[ $key ] = get_post_meta( get_the_ID(), PledgeMeta\META_PREFIX . $key, $config['single'] );
|
||||
}
|
||||
|
||||
$contributors = Contributor\get_pledge_contributors( get_the_ID() );
|
||||
$count = count( $contributors );
|
||||
|
||||
$content = apply_filters( 'the_content', $data['org-description'] );
|
||||
|
||||
$contributor_title = sprintf(
|
||||
esc_html(
|
||||
_n( '%1$s has pledged %2$d contributor', '%1$s has pledged %2$d contributors', $count, 'wordpressorg' )
|
||||
),
|
||||
wp_kses_post( get_the_title() ),
|
||||
intval( $count )
|
||||
);
|
||||
?>
|
||||
|
||||
<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
|
||||
<div class="entry-image">
|
||||
<?php if ( has_post_thumbnail() ) : ?>
|
||||
<div class="entry-image__logo">
|
||||
<?php the_post_thumbnail(); ?>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="entry-image__placeholder"></div>
|
||||
<?php endif; ?>
|
||||
</div><!-- .post-thumbnail -->
|
||||
|
||||
<header class="entry-header">
|
||||
<?php if ( is_singular() ) : ?>
|
||||
|
||||
<?php the_title( '<h1 class="entry-title">', '</h1>' ); ?>
|
||||
|
||||
<?php else : ?>
|
||||
|
||||
<?php the_title( '<h2 class="entry-title"><a href="' . esc_url( get_permalink() ) . '" rel="bookmark">', '</a></h2>' ); ?>
|
||||
|
||||
<?php endif; ?>
|
||||
</header><!-- .entry-header -->
|
||||
|
||||
<div class="entry-content">
|
||||
<?php
|
||||
echo wp_kses_post( $content );
|
||||
?>
|
||||
|
||||
<div class="pledge-contributors">
|
||||
<?php /* phpcs:ignore -- escaped above */ ?>
|
||||
<h3><?php echo $contributor_title ?></h3>
|
||||
|
||||
<?php
|
||||
foreach ( $contributors as $contrib_post ) {
|
||||
$contrib = get_user_by( 'login', $contrib_post->post_title );
|
||||
if ( $contrib ) {
|
||||
printf(
|
||||
'<span class="pledge-contributor__avatar">%s</span>',
|
||||
get_avatar( $contrib->user_email, 30, 'blank' )
|
||||
);
|
||||
}
|
||||
}
|
||||
?>
|
||||
</div><!-- .pledge-contributors -->
|
||||
|
||||
</div><!-- .entry-content -->
|
||||
</article><!-- #post-## -->
|
Loading…
Reference in a new issue