Skip to content

Commit

Permalink
Merge pull request #763 from Kit/restrict-content-tags-use-signed-sub…
Browse files Browse the repository at this point in the history
…scriber-id

Member Content: Tags: Use Signed Subscriber ID
  • Loading branch information
n7studios authored Jan 23, 2025
2 parents 2406dbf + a73a4fd commit b80ce66
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 40 deletions.
5 changes: 3 additions & 2 deletions .env.dist.testing
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ TEST_SITE_WP_DOMAIN=127.0.0.1
[email protected]
TEST_SITE_HTTP_USER_AGENT=HeadlessChrome
TEST_SITE_HTTP_USER_AGENT_MOBILE=HeadlessChromeMobile
CONVERTKIT_API_SUBSCRIBER_EMAIL="[email protected]"
CONVERTKIT_API_SUBSCRIBER_ID="1579118532"
CONVERTKIT_API_SUBSCRIBER_ID_NO_ACCESS="1632998602"
CONVERTKIT_API_FORM_NAME="Page Form [inline]"
CONVERTKIT_API_FORM_ID="2765139"
CONVERTKIT_API_FORM_FORMAT_MODAL_NAME="Modal Form [modal]"
Expand Down Expand Up @@ -47,8 +50,6 @@ CONVERTKIT_API_SEQUENCE_NAME="WordPress Sequence"
CONVERTKIT_API_SEQUENCE_ID="1030824"
CONVERTKIT_API_TAG_NAME="wordpress"
CONVERTKIT_API_TAG_ID="2744672"
CONVERTKIT_API_SUBSCRIBER_EMAIL="[email protected]"
CONVERTKIT_API_SUBSCRIBER_ID="1579118532"
CONVERTKIT_API_THIRD_PARTY_INTEGRATIONS_FORM_NAME="Third Party Integrations Form [inline]"
CONVERTKIT_API_THIRD_PARTY_INTEGRATIONS_FORM_ID="3003590"
CONVERTKIT_API_COMMERCE_JS_URL="https://cheerful-architect-3237.kit.com/commerce.js"
Expand Down
5 changes: 3 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ CONVERTKIT_API_KEY=
CONVERTKIT_API_SECRET=
CONVERTKIT_API_SIGNED_SUBSCRIBER_ID=
CONVERTKIT_API_SIGNED_SUBSCRIBER_ID_NO_ACCESS=
CONVERTKIT_API_SUBSCRIBER_EMAIL="[email protected]"
CONVERTKIT_API_SUBSCRIBER_ID="1579118532"
CONVERTKIT_API_SUBSCRIBER_ID_NO_ACCESS="1632998602"
CONVERTKIT_API_RECAPTCHA_SITE_KEY=
CONVERTKIT_API_RECAPTCHA_SECRET_KEY=
CONVERTKIT_API_FORM_NAME="Page Form [inline]"
Expand Down Expand Up @@ -55,8 +58,6 @@ CONVERTKIT_API_SEQUENCE_NAME="WordPress Sequence"
CONVERTKIT_API_SEQUENCE_ID="1030824"
CONVERTKIT_API_TAG_NAME="wordpress"
CONVERTKIT_API_TAG_ID="2744672"
CONVERTKIT_API_SUBSCRIBER_EMAIL="[email protected]"
CONVERTKIT_API_SUBSCRIBER_ID="1579118532"
CONVERTKIT_API_THIRD_PARTY_INTEGRATIONS_FORM_NAME="Third Party Integrations Form [inline]"
CONVERTKIT_API_THIRD_PARTY_INTEGRATIONS_FORM_ID="3003590"
CONVERTKIT_API_COMMERCE_JS_URL="https://cheerful-architect-3237.kit.com/commerce.js"
Expand Down
138 changes: 102 additions & 36 deletions includes/class-convertkit-output-restrict-content.php
Original file line number Diff line number Diff line change
Expand Up @@ -829,54 +829,120 @@ private function subscriber_has_access( $subscriber_id ) { // phpcs:ignore Gener
// for restrict by tag and form later.
switch ( $this->resource_type ) {
case 'product':
// Get products that the subscriber has access to.
$result = $this->api->profile( $subscriber_id );
// For products, the subscriber ID has to be a signed subscriber ID string.
return $this->subscriber_has_access_to_product_by_signed_subscriber_id( $subscriber_id, absint( $this->resource_id ) );

// If an error occured, the subscriber ID is invalid.
if ( is_wp_error( $result ) ) {
return false;
case 'tag':
// If the subscriber ID is numeric, check using get_subscriber_tags().
if ( is_numeric( $subscriber_id ) ) {
return $this->subscriber_has_access_to_tag_by_subscriber_id( $subscriber_id, absint( $this->resource_id ) );
}

// If no products exist, there's no access.
if ( ! $result['products'] || ! count( $result['products'] ) ) {
return false;
}
// The subscriber ID is a signed subscriber ID string.
// Check using profile().
return $this->subscriber_has_access_to_tag_by_signed_subscriber_id( $subscriber_id, absint( $this->resource_id ) );

// Return if the subscriber is not subscribed to the product.
if ( ! in_array( absint( $this->resource_id ), $result['products'], true ) ) {
return false;
}
}

// If here, the subscriber is subscribed to the product.
return true;
// If here, the subscriber does not have access.
return false;

case 'tag':
// Get tags that the subscriber has been assigned.
$tags = $this->api->get_subscriber_tags( $subscriber_id );
}

// If an error occured, the subscriber ID is invalid.
if ( is_wp_error( $tags ) ) {
return false;
}
/**
* Determines if the given signed subscriber ID has an active subscription to
* the given product.
*
* @since 2.7.1
*
* @param string $signed_subscriber_id Signed Subscriber ID.
* @param int $product_id Product ID.
* @return bool Has access to product
*/
private function subscriber_has_access_to_product_by_signed_subscriber_id( $signed_subscriber_id, $product_id ) {

// If no tags exist, there's no access.
if ( ! count( $tags['tags'] ) ) {
return false;
}
// Get products that the subscriber has access to.
$result = $this->api->profile( $signed_subscriber_id );

// Iterate through the subscriber's tags to see if they have the required tag.
foreach ( $tags['tags'] as $tag ) {
if ( $tag['id'] === absint( $this->resource_id ) ) {
// Subscriber has the required tag assigned to them - grant access.
return true;
}
}
// If an error occured, the subscriber ID is invalid.
if ( is_wp_error( $result ) ) {
return false;
}

// If here, the subscriber does not have the tag.
return false;
// If no products exist, there's no access.
if ( ! $result['products'] || ! count( $result['products'] ) ) {
return false;
}

// If here, the subscriber does not have access.
// Return if the subscriber is subscribed to the product or not.
return in_array( $product_id, $result['products'], true );

}

/**
* Determines if the given signed subscriber ID has an active subscription to
* the given tag.
*
* @since 2.7.1
*
* @param string $signed_subscriber_id Signed Subscriber ID.
* @param int $tag_id Tag ID.
* @return bool Has access to tag
*/
private function subscriber_has_access_to_tag_by_signed_subscriber_id( $signed_subscriber_id, $tag_id ) {

// Get products that the subscriber has access to.
$result = $this->api->profile( $signed_subscriber_id );

// If an error occured, the subscriber ID is invalid.
if ( is_wp_error( $result ) ) {
return false;
}

// If no tags exist, there's no access.
if ( ! $result['tags'] || ! count( $result['tags'] ) ) {
return false;
}

// Return if the subscriber is subscribed to the tag or not.
return in_array( $tag_id, $result['tags'], true );

}

/**
* Determines if the given signed subscriber ID has an active subscription to
* the given tag.
*
* @since 2.7.1
*
* @param int $subscriber_id Subscriber ID.
* @param int $tag_id Tag ID.
* @return bool Has access to tag
*/
private function subscriber_has_access_to_tag_by_subscriber_id( $subscriber_id, $tag_id ) {

// Get tags that the subscriber has been assigned.
$tags = $this->api->get_subscriber_tags( $subscriber_id );

// If an error occured, the subscriber ID is invalid.
if ( is_wp_error( $tags ) ) {
return false;
}

// If no tags exist, there's no access.
if ( ! count( $tags['tags'] ) ) {
return false;
}

// Iterate through the subscriber's tags to see if they have the required tag.
foreach ( $tags['tags'] as $tag ) {
if ( $tag['id'] === $tag_id ) {
// Subscriber has the required tag assigned to them - grant access.
return true;
}
}

// If here, the subscriber does not have the tag.
return false;

}
Expand Down
93 changes: 93 additions & 0 deletions tests/_support/Helper/Acceptance/ConvertKitRestrictContent.php
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,22 @@ public function testRestrictedContentByTagOnFrontend($I, $urlOrPageID, $emailAdd
$I->see($options['text_items']['subscribe_text_tag']);
$I->seeInSource('<input type="submit" class="wp-block-button__link wp-block-button__link' . ( $recaptchaEnabled ? ' g-recaptcha' : '' ) . '" value="' . $options['text_items']['subscribe_button_label'] . '"');

// Set cookie with subscriber ID that does not have access to the tag, and reload the restricted content page.
$I->setCookie('ck_subscriber_id', $_ENV['CONVERTKIT_API_SUBSCRIBER_ID_NO_ACCESS']);
if ( is_numeric( $urlOrPageID ) ) {
$I->amOnPage('?p=' . $urlOrPageID . '&ck-cache-bust=' . microtime() );
} else {
$I->amOnUrl($urlOrPageID . '?ck-cache-bust=' . microtime() );
}

// Confirm an inline error message is displayed.
$I->seeInSource('<div class="convertkit-restrict-content-notice convertkit-restrict-content-notice-error">' . $options['text_items']['no_access_text'] . '</div>');
$I->seeInSource('<div id="convertkit-restrict-content-email-field" class="convertkit-restrict-content-error">');

// Confirm that the visible text displays, hidden text does not display and the CTA displays.
$I->see($options['visible_content']);
$I->dontSee($options['member_content']);

// Enter the email address and submit the form.
$I->fillField('convertkit_email', $emailAddress);
$I->click('input.wp-block-button__link');
Expand All @@ -399,6 +415,83 @@ public function testRestrictedContentByTagOnFrontend($I, $urlOrPageID, $emailAdd
$I->testRestrictContentDisplaysContent($I, $options);
}

/**
* Run frontend tests for restricted content by ConvertKit Product, to confirm that visible and member's content
* is / is not displayed when using signed subscriber IDs that do / do not have access to the content.
*
* @since 2.7.1
*
* @param AcceptanceTester $I Tester.
* @param string|int $urlOrPageID URL or ID of Restricted Content Page.
* @param bool|array $options {
* Optional. An array of settings.
*
* @type string $visible_content Content that should always be visible.
* @type string $member_content Content that should only be available to authenticated subscribers.
* @type array $text_items Expected text for subscribe text, subscribe button label, email text etc. If not defined, uses expected defaults.
* }
* @param bool $recaptchaEnabled Whether the reCAPTCHA settings are enabled in the Plugin settings.
*/
public function testRestrictedContentByTagOnFrontendUsingSignedSubscriberID($I, $urlOrPageID, $options = false, $recaptchaEnabled = false)
{
// Merge options with defaults.
$options = $this->_getRestrictedContentOptionsWithDefaultsMerged($options);

// Navigate to the page.
if ( is_numeric( $urlOrPageID ) ) {
$I->amOnPage('?p=' . $urlOrPageID);
} else {
$I->amOnUrl($urlOrPageID);
}

// Clear any existing cookie from a previous test and reload.
$I->resetCookie('ck_subscriber_id');
$I->reloadPage();

// Check that no PHP warnings or notices were output.
$I->checkNoWarningsAndNoticesOnScreen($I);

// Confirm Restrict Content CSS is output.
$I->seeInSource('<link rel="stylesheet" id="convertkit-restrict-content-css" href="' . $_ENV['TEST_SITE_WP_URL'] . '/wp-content/plugins/convertkit/resources/frontend/css/restrict-content.css');

// Confirm that the visible text displays, hidden text does not display and the CTA displays.
$I->see($options['visible_content']);
$I->dontSee($options['member_content']);

// Confirm that the CTA displays with the expected headings, text and other elements.
$I->seeElementInDOM('#convertkit-restrict-content');
$I->seeInSource('<h3>' . $options['text_items']['subscribe_heading_tag'] . '</h3>');
$I->see($options['text_items']['subscribe_text_tag']);
$I->seeInSource('<input type="submit" class="wp-block-button__link wp-block-button__link' . ( $recaptchaEnabled ? ' g-recaptcha' : '' ) . '" value="' . $options['text_items']['subscribe_button_label'] . '"');

// Set cookie with signed subscriber ID that does not have access to the tag, and reload the restricted content page.
$I->setCookie('ck_subscriber_id', $_ENV['CONVERTKIT_API_SIGNED_SUBSCRIBER_ID_NO_ACCESS']);
if ( is_numeric( $urlOrPageID ) ) {
$I->amOnPage('?p=' . $urlOrPageID . '&ck-cache-bust=' . microtime() );
} else {
$I->amOnUrl($urlOrPageID . '?ck-cache-bust=' . microtime() );
}

// Confirm an inline error message is displayed.
$I->seeInSource('<div class="convertkit-restrict-content-notice convertkit-restrict-content-notice-error">' . $options['text_items']['no_access_text'] . '</div>');
$I->seeInSource('<div id="convertkit-restrict-content-email-field" class="convertkit-restrict-content-error">');

// Confirm that the visible text displays, hidden text does not display and the CTA displays.
$I->see($options['visible_content']);
$I->dontSee($options['member_content']);

// Set cookie with signed subscriber ID that does have access to the tag, and reload the restricted content page.
$I->setCookie('ck_subscriber_id', $_ENV['CONVERTKIT_API_SIGNED_SUBSCRIBER_ID']);
if ( is_numeric( $urlOrPageID ) ) {
$I->amOnPage('?p=' . $urlOrPageID . '&ck-cache-bust=' . microtime() );
} else {
$I->amOnUrl($urlOrPageID . '?ck-cache-bust=' . microtime() );
}

// Confirm that the restricted content is now displayed.
$I->testRestrictContentDisplaysContent($I, $options);
}

/**
* Run frontend tests for restricted content, to confirm that both visible and member content is displayed
* when a valid signed subscriber ID is set as a cookie, as if the user entered a code sent in the email.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,41 @@ public function testRestrictContentByTag(AcceptanceTester $I)
$I->testRestrictedContentByTagOnFrontend($I, $url, $I->generateEmailAddress());
}

/**
* Test that restricting content by a Tag specified in the Page Settings works when
* creating and viewing a new WordPress Page and the subscriber uses a signed subscriber ID.
*
* @since 2.7.1
*
* @param AcceptanceTester $I Tester.
*/
public function testRestrictContentByTagUsingSignedSubscriberID(AcceptanceTester $I)
{
// Add a Page using the Gutenberg editor.
$I->addGutenbergPage($I, 'page', 'Kit: Page: Restrict Content: Tag by Signed Subscriber ID');

// Configure metabox's Restrict Content setting = Tag name.
$I->configureMetaboxSettings(
$I,
'wp-convertkit-meta-box',
[
'form' => [ 'select2', 'None' ],
'restrict_content' => [ 'select2', $_ENV['CONVERTKIT_API_TAG_NAME'] ],
]
);

// Add blocks.
$I->addGutenbergParagraphBlock($I, 'Visible content.');
$I->addGutenbergBlock($I, 'More', 'more');
$I->addGutenbergParagraphBlock($I, 'Member-only content.');

// Publish Page.
$url = $I->publishGutenbergPage($I);

// Test Restrict Content functionality.
$I->testRestrictedContentByTagOnFrontendUsingSignedSubscriberID($I, $url);
}

/**
* Test that restricting content by a Tag that does not exist does not output
* a fatal error and instead displays all of the Page's content.
Expand Down

0 comments on commit b80ce66

Please sign in to comment.