Skip to content

Instantly share code, notes, and snippets.

@boonebgorges
Last active October 5, 2023 17:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save boonebgorges/60467a056ec9f26a044ace3e4d52fdc9 to your computer and use it in GitHub Desktop.
Save boonebgorges/60467a056ec9f26a044ace3e4d52fdc9 to your computer and use it in GitHub Desktop.
Migrate to cac-cv-editor
<?php
/**
* Standardizes the value of a social field.
*
* @param string $value The value of the field.
* @param string $service The service that the field is for.
* @return string
*/
function standardize_social_field_value( $value, $service ) {
$value = trim( $value );
switch ( $service ) {
// Twitter: Remove @ symbol.
case 'twitter' :
$value = ltrim( $value, '@' );
break;
}
// Enforce fully-qualified URLs.
$url_fields = [ 'twitter', 'github', 'linkedin', 'facebook' ];
if ( in_array( $service, $url_fields, true ) && ! preg_match( '#^https?://#', $value ) ) {
$value = 'https://' . $value;
}
return $value;
}
/**
* Gets a row for the contact section ("Find Me Online", "Contact", etc)
*
* @param string $title The title of the row, to appear in the left-hand column.
* @param array $inner_block The inner block for the row, to appear in the right-hand column.
*/
function get_columns_for_contact_row( $title, $inner_block ) {
$left_column_block = [
'blockName' => 'core/column',
'attrs' => [
'width' => '33.33%',
],
'innerBlocks' => [
[
'blockName' => 'core/paragraph',
'attrs' => [],
'innerBlocks' => [],
'innerHTML' => '<p>' . $title . '</p>',
'innerContent' => [
'<p>' . $title . '</p>',
],
]
],
'innerHTML' => '<div class="wp-block-column" style="flex-basis:33.33%"></div>',
'innerContent' => [
'<div class="wp-block-column" style="flex-basis:33.33%">',
null,
'</div>',
],
];
$right_column_block = [
'blockName' => 'core/column',
'attrs' => [
'width' => '66.66%',
],
'innerBlocks' => [
$inner_block,
],
'innerHTML' => '<div class="wp-block-column" style="flex-basis:66.66%"></div>',
'innerContent' => [
'<div class="wp-block-column" style="flex-basis:66.66%">',
null,
'</div>',
],
];
$row_columns_block = [
'blockName' => 'core/columns',
'attrs' => [],
'innerBlocks' => [
$left_column_block,
$right_column_block,
],
'innerHTML' => '<div class="wp-block-columns"></div>',
'innerContent' => [
'<div class="wp-block-columns">',
null,
null,
'</div>',
],
];
return $row_columns_block;
}
function get_profile_image_block_for_user_avatar( $user_id ) {
$user_avatar_url = bp_core_fetch_avatar(
[
'item_id' => $user_id,
'html' => false,
'type' => 'full',
]
);
// If the URL is a schemeless Gravatar URL, make it fully qualified.
if ( 0 === strpos( $user_avatar_url, '//www.gravatar.com' ) ) {
$user_avatar_url = 'https:' . $user_avatar_url;
}
$tmp = download_url( $user_avatar_url );
// Add a unique filename part before storing.
$filename = wp_generate_uuid4() . '-' . basename( $user_avatar_url );
$file_array = [
'name' => $filename,
'tmp_name' => $tmp,
];
// Generate the attachment post object.
$image_data = [
'url' => $user_avatar_url,
'title' => sprintf( 'Avatar for %s', bp_core_get_user_displayname( $user_id ) ),
'caption' => '',
'alt' => sprintf( 'Avatar for %s', bp_core_get_user_displayname( $user_id ) ),
];
add_filter( 'upload_dir', 'CAC\CAC_CV_Editor\Media\filter_upload_dir' );
$overrides = array(
'test_form' => false,
);
$file = wp_handle_sideload( $file_array, $overrides );
if ( is_wp_error( $file ) ) {
return '';
}
$image_block_data = [
'blockName' => 'cac/cv-profile-image',
'attrs' => [
'imageURL' => $file['url'],
],
'innerBlocks' => [],
'innerHTML' => '',
'innerContent' => [],
];
return $image_block_data;
}
/**
* Converts text to blocks.
*
* Parses the following block-level elements and converts them to blocks:
* - Paragraphs
* - Headings
* - Lists (ul, li)
*
* Inline elements are left as-is.
*
* @param string $text The text to convert.
* @return array
*/
function convert_text_to_blocks( $text ) {
$dom = new DOMDocument();
$dom->loadHTML( $text );
$element_map = [
'p' => [
'blockName' => 'core/paragraph',
],
'h1' => [
'blockName' => 'core/heading',
'attrs' => [
'level' => 1,
],
],
'h2' => [
'blockName' => 'core/heading',
'attrs' => [
'level' => 2,
],
],
'h3' => [
'blockName' => 'core/heading',
'attrs' => [
'level' => 3,
],
],
'ul' => [
'blockName' => 'core/list',
'attrs' => [
'ordered' => false,
],
],
'ol' => [
'blockName' => 'core/list',
'attrs' => [
'ordered' => true,
],
],
'li' => [
'blockName' => 'core/list-item',
],
];
$xpath = new DOMXPath( $dom );
// Identify and group orphaned 'li' elements.
$listItems = $xpath->query('//li');
$group = [];
foreach ( $listItems as $item ) {
$parent = $item->parentNode->nodeName;
if ( $parent !== 'ul' && $parent !== 'ol' ) {
$group[] = $item;
}
$nextSibling = $item->nextSibling;
// Check for adjacent orphaned <li> elements.
while ( $nextSibling && ( $nextSibling->nodeType === XML_TEXT_NODE && trim($nextSibling->nodeValue) === '' ) ) {
$nextSibling = $nextSibling->nextSibling;
}
// If there is no adjacent orphaned <li> or we're at the end of the loop, wrap the group in a <ul>.
if ( ! $nextSibling || ( $nextSibling->nodeName !== 'li' || $nextSibling->parentNode->nodeName === 'ul' || $nextSibling->parentNode->nodeName === 'ol' ) || empty( $group ) ) {
if ( ! empty( $group ) ) {
$ul = $dom->createElement('ul');
$group[0]->parentNode->insertBefore( $ul, $group[0] );
foreach ( $group as $orphanItem ) {
$ul->appendChild( $orphanItem );
}
$group = [];
}
}
}
// For each element type, find matching elements, and surround with block markup.
foreach ( $element_map as $tag_name => $block_type ) {
$elements = $xpath->query( '//' . $tag_name );
for ( $i = $elements->length - 1; $i >= 0; $i-- ) {
$element = $elements->item( $i );
// Create start and end comment nodes.
$start_comment = $dom->createComment( ' wp:' . $block_type['blockName'] . ' ' );
$end_comment = $dom->createComment( ' /wp:' . $block_type['blockName'] . ' ' );
// Insert comment nodes before and after the element.
$element->parentNode->insertBefore( $start_comment, $element );
$element->parentNode->insertBefore( $end_comment, $element->nextSibling );
}
}
$body = $dom->getElementsByTagName( 'body' )->item( 0 );
$innerHTML = '';
foreach ( $body->childNodes as $child ) {
$innerHTML .= $dom->saveHTML( $child );
}
return $innerHTML;
}
/**
* Get the cv-top block for a user.
*
* @param int $user_id The user ID.
* @return array
*/
function get_cv_top_block( $user_id ) {
// Avatar image.
$image_column_block = [
'blockName' => 'core/column',
'attrs' => [
'width' => '20%',
],
'innerBlocks' => [
get_profile_image_block_for_user_avatar( $user_id ),
],
'innerHTML' => '<div class="wp-block-column" style="flex-basis:20%"></div>',
'innerContent' => [
'<div class="wp-block-column" style="flex-basis:20%">',
null,
'</div>',
],
];
// cv-shortlink
// Throughout, please format arrays with aligning arrows, like so:
$cv_shortlink_block = [
'blockName' => 'cac/cv-shortlink',
'attrs' => [
'shortlink' => get_user_meta( $user_id, 'cac_yourls_shorturl', true ),
'userId' => $user_id,
],
'innerBlocks' => [],
'innerHTML' => '',
'innerContent' => [
null,
],
];
$cv_shortlink_column = [
'blockName' => 'core/column',
'innerBlocks' => [
$cv_shortlink_block,
],
'innerHTML' => '<div class="wp-block-column"></div>',
'innerContent' => [
'<div class="wp-block-column">',
null,
'</div>',
],
];
$cv_last_active = [
'blockName' => 'cac/cv-last-active',
'attrs' => [
'userId' => $user_id,
],
'innerBlocks' => [],
'innerHTML' => '',
'innerContent' => [],
];
$cv_last_active_column = [
'blockName' => 'core/column',
'innerBlocks' => [
$cv_last_active,
],
'innerHTML' => '<div class="wp-block-column"></div>',
'innerContent' => [
'<div class="wp-block-column">',
null,
'</div>',
],
];
$right_upper_columns = [
'blockName' => 'core/columns',
'attrs' => [],
'innerBlocks' => [
$cv_shortlink_column,
$cv_last_active_column,
],
'innerHTML' => '<div class="wp-block-columns"></div>',
'innerContent' => [
'<div class="wp-block-columns">',
null,
null,
'</div>',
],
];
remove_filter( 'get_the_author_display_name', 'cac_add_pronouns_to_display_name_wp', 10, 2 );
$user_display_name = bp_core_get_user_displayname( $user_id );
add_filter( 'get_the_author_display_name', 'cac_add_pronouns_to_display_name_wp', 10, 2 );
$name_heading_block = [
'blockName' => 'core/heading',
'attrs' => [
'level' => 2,
'placeholder' => 'Enter your name',
],
'innerBlocks' => [],
'innerHTML' => '<h2>' . $user_display_name . '</h2>',
'innerContent' => [
'<h2>' . $user_display_name . '</h2>',
],
];
// Pronouns.
$pronouns = cac_get_user_gender_pronouns( $user_id );
$pronouns_block = [
'blockName' => 'core/paragraph',
'attrs' => [],
'innerBlocks' => [],
'innerHTML' => '<p>' . $pronouns . '</p>',
'innerContent' => [
'<p>' . $pronouns . '</p>',
],
];
// One-line bio.
$one_line_bio = xprofile_get_field_data( 'One-Line Bio', $user_id );
$one_line_bio_block = [
'blockName' => 'core/heading',
'attrs' => [
'level' => 3,
],
'innerBlocks' => [],
'innerHTML' => '<h3>' . $one_line_bio . '</h3>',
'innerContent' => [
'<h3>' . $one_line_bio . '</h3>',
],
];
// cac/cv-bio
$about_you = xprofile_get_field_data( 'About You', $user_id );
$cv_bio_block = [
'blockName' => 'cac/cv-bio',
'attrs' => [],
'innerBlocks' => [
[
'blockName' => 'core/paragraph',
'attrs' => [
'placeholder' => 'Enter a short bio paragraph or two',
],
'innerBlocks' => [],
'innerHTML' => '<p>' . $about_you . '</p>',
'innerContent' => [
'<p>' . $about_you . '</p>',
],
],
],
'innerHTML' => '',
'innerContent' => [
'<div class="wp-block-cac-cv-bio">',
null,
'</div>',
],
];
$bio_group_block = [
'blockName' => 'core/group',
'attrs' => [
'layout' => [
'type' => 'constrained',
],
],
'innerBlocks' => [
$cv_bio_block,
],
'innerHTML' => '<div class="wp-block-group"></div>',
'innerContent' => [
'<div class="wp-block-group">',
null,
'</div>',
],
];
$cv_profile_navigation_block = [
'blockName' => 'cac/cv-profile-navigation',
'attrs' => [
'userId' => $user_id,
],
'innerBlocks' => [],
'innerHTML' => '',
'innerContent' => [],
];
// Social links.
// @todo Google Scholar; Orcid; Academia.edu; Delicious
$social_fields = [
[
'service' => 'twitter',
'xprofile_field_id' => xprofile_get_field_id_from_name( 'Twitter' ),
],
[
'service' => 'github',
'xprofile_field_id' => xprofile_get_field_id_from_name( 'Github' ),
],
[
'service' => 'linkedin',
'xprofile_field_id' => xprofile_get_field_id_from_name( 'LinkedIn Profile Link' ),
],
[
'service' => 'facebook',
'xprofile_field_id' => xprofile_get_field_id_from_name( 'Facebook Profile Link' ),
],
[
'service' => 'vimeo',
'xprofile_field_id' => xprofile_get_field_id_from_name( 'Vimeo' ),
],
];
$social_links = array_map(
function( $field ) use ( $user_id ) {
$field_value = xprofile_get_field_data( $field['xprofile_field_id'], $user_id );
if ( ! $field_value ) {
return null;
}
$field_value = standardize_social_field_value( $field_value, $field['service'] );
return [
'blockName' => 'core/social-link',
'attrs' => [
'url' => $field_value,
'service' => $field['service'],
],
'innerBlocks' => [],
'innerHTML' => '',
'innerContent' => [],
];
},
$social_fields
);
$social_links = array_filter( $social_links );
$social_links_inner_content = [ '<ul class="wp-block-social-links">' ];
$social_links_inner_content = array_merge( $social_links_inner_content, array_fill( 0, count( $social_links ), null ) );
$social_links_inner_content[] = '</ul>';
$social_links_block = [
'blockName' => 'core/social-links',
'attrs' => [],
'innerBlocks' => $social_links,
'innerHTML' => '<ul class="wp-block-social-links"></ul>',
'innerContent' => $social_links_inner_content,
];
$social_links_row_columns = get_columns_for_contact_row( 'Find Me Online', $social_links_block );
$user_phone = xprofile_get_field_data( 'Phone', $user_id );
$user_public_email = xprofile_get_field_data( 'Public Email', $user_id );
$user_skype_id = xprofile_get_field_data( 'Skype ID', $user_id );
$user_im = xprofile_get_field_data( 'IM', $user_id );
$contact_chunks = [];
if ( $user_phone ) {
$contact_chunks[] = $user_phone;
}
if ( $user_public_email ) {
$contact_chunks[] = sprintf( '<a href="mailto:%s">%s</a>', $user_public_email, $user_public_email );
}
if ( $user_skype_id ) {
$contact_chunks[] = sprintf( 'Skype: <a href="skype:%s">%s</a>', $user_skype_id, $user_skype_id );
}
if ( $user_im ) {
$contact_chunks[] = sprintf( 'IM: %s', $user_im );
}
$contact_row_content = implode( ' &middot; ', $contact_chunks );
$contact_row_columns = get_columns_for_contact_row(
'Contact',
[
'blockName' => 'core/paragraph',
'attrs' => [
'placeholder' => 'Enter phone and/or email address',
],
'innerBlocks' => [],
'innerHTML' => '<p>' . $contact_row_content . '</p>',
'innerContent' => [
'<p>' . $contact_row_content . '</p>',
],
]
);
$website_url = xprofile_get_field_data( 'Website', $user_id );
$website_url = standardize_social_field_value( $website_url, 'website' );
$website_row_columns = get_columns_for_contact_row(
'Website',
[
'blockName' => 'core/paragraph',
'attrs' => [
'placeholder' => 'Enter URL',
],
'innerBlocks' => [],
'innerHTML' => '<p><a href="' . esc_attr( $website_url ) . '">' . esc_html( $website_url ) . '</a></p>',
'innerContent' => [
'<p><a href="' . esc_attr( $website_url ) . '">' . esc_html( $website_url ) . '</a></p>',
],
]
);
$blog_url = xprofile_get_field_data( 'Blog', $user_id );
$blog_url = standardize_social_field_value( $blog_url, 'blog' );
$blog_row_columns = get_columns_for_contact_row(
'Blog',
[
'blockName' => 'core/paragraph',
'attrs' => [
'placeholder' => 'Enter URL',
],
'innerBlocks' => [],
'innerHTML' => '<p><a href="' . esc_attr( $blog_url ) . '">' . esc_html( $blog_url ) . '</a></p>',
'innerContent' => [
'<p><a href="' . esc_attr( $blog_url ) . '">' . esc_html( $blog_url ) . '</a></p>',
],
]
);
$right_column_inner_blocks = [
$right_upper_columns,
$name_heading_block,
$pronouns_block,
$one_line_bio_block,
$bio_group_block,
$cv_profile_navigation_block,
$social_links_row_columns,
$contact_row_columns,
$website_row_columns,
$blog_row_columns,
];
$right_column_inner_content = [ '<div class="wp-block-column" style="flex-basis:80%">' ];
$right_column_inner_content = array_merge( $right_column_inner_content, array_fill( 0, count( $right_column_inner_blocks ), null ) );
$right_column_inner_content[] = '</div>';
$right_column = [
'blockName' => 'core/column',
'attrs' => [
'width' => '80%',
],
'innerBlocks' => $right_column_inner_blocks,
'innerHTML' => '<div class="wp-block-column" style="flex-basis:80%"></div>',
'innerContent' => $right_column_inner_content,
];
$top_columns_inner_blocks = [
$image_column_block,
$right_column,
];
$top_columns_inner_content = [ '<div class="wp-block-columns">' ];
$top_columns_inner_content = array_merge( $top_columns_inner_content, array_fill( 0, count( $top_columns_inner_blocks ), null ) );
$top_columns_inner_content[] = '</div>';
$top_columns = [
'blockName' => 'core/columns',
'attrs' => [],
'innerBlocks' => $top_columns_inner_blocks,
'innerHTML' => '<div class="wp-block-columns"></div>',
'innerContent' => $top_columns_inner_content,
];
$cv_top = [
'blockName' => 'cac/cv-top',
'attrs' => [],
'innerBlocks' => [
$top_columns
],
'innerHTML' => '<div class="wp-block-cac-cv-top"></div>',
'innerContent' => [
'<div class="wp-block-cac-cv-top">',
null,
'</div>',
],
];
return $cv_top;
}
/**
* Get cv-rows block for a user.
*
* @param int $user_id The user ID.
* @return array
*/
function get_cv_rows_block( $user_id ) {
$cv_rows = [];
$cacap_data = get_usermeta( $user_id, 'cacap_widget_instance_data', true );
$cac_campuses = cac_get_cuny_campuses();
$widgets = [];
foreach ( $cacap_data as $key => $value ) {
$widget = new CACAP_Widget_Instance( $value );
switch ( $widget->widget_type->slug ) {
case 'titlewidget' :
case 'college' :
// do nothing.
break;
case 'positions' :
$position_blocks = array_map(
function ( $position ) use ( $cac_campuses ) {
$campus_name = $position['college'] ?? '';
if ( '*Non-CUNY' === $campus_name ) {
$campus_slug = 'non-cuny';
} else {
$campus_slug = '';
foreach ( $cac_campuses as $campus_slug => $campus ) {
if ( $campus['full_name'] === $campus_name ) {
break;
}
}
}
$department = $position['department'] ?? '';
$title = $position['title'] ?? '';
$inner_html = '';
$inner_content = [ '<div class="wp-block-cac-cv-position">' ];
if ( $title ) {
$trailing_comma = $department || $campus_name ? ', ' : '';
$inner_html .= '<span class="position-title">' . $title . $trailing_comma . '</span>';
$inner_content[] = '<span class="position-title">';
$inner_content[] = null;
$inner_content[] = '</span>';
}
if ( $department ) {
$trailing_comma = $campus_name ? ', ' : '';
$inner_html .= '<span class="position-department">' . $department . $trailing_comma . '</span>';
$inner_content[] = '<span class="position-department">';
$inner_content[] = null;
$inner_content[] = '</span>';
}
if ( $campus_name ) {
$inner_html .= '<span class="position-campus">' . $campus_name . '</span>';
$inner_content[] = '<span class="position-campus">';
$inner_content[] = null;
$inner_content[] = '</span>';
}
$position_block = [
'blockName' => 'cac/cv-position',
'attrs' => [
'campus' => $campus_slug,
'department' => $department,
'title' => $title,
],
'innerBlocks' => [],
'innerHTML' => '<div class="wp-block-cac-cv-position">' . $inner_html . '</div>',
'innerContent' => [ '<div class="wp-block-cac-cv-position">' . $inner_html . '</div>' ],
];
return $position_block;
},
$widget->value
);
$positions_block_inner_content = [ '<div class="wp-block-cac-cv-positions">' ];
$positions_block_inner_content = array_merge( $positions_block_inner_content, array_fill( 0, count( $position_blocks ), null ) );
$positions_block_inner_content[] = '</div>';
$positions_block = [
'blockName' => 'cac/cv-positions',
'attrs' => [],
'innerBlocks' => $position_blocks,
'innerHTML' => '<div class="wp-block-cac-cv-positions"></div>',
'innerContent' => $positions_block_inner_content,
];
$header_fragment = '<div class="cac-cv-row-header"><h2>Positions</h2></div>';
$cv_row_block = [
'blockName' => 'cac/cv-row',
'attrs' => [
'content' => 'Positions',
],
'innerBlocks' => [
$positions_block,
],
'innerHTML' => '<div class="wp-block-cac-cv-row">' . $header_fragment . '<div class="cac-cv-row-content"></div></div>',
'innerContent' => [
'<div class="wp-block-cac-cv-row">' . $header_fragment . '<div class="cac-cv-row-content">',
null,
'</div></div>',
],
];
$cv_rows[] = $cv_row_block;
break;
default :
$text = $widget->display_content();
$text = convert_text_to_blocks( $text );
$header_fragment = '<div class="cac-cv-row-header"><h2>' . $widget->display_title() . '</h2></div>';
$cv_row_block = [
'blockName' => 'cac/cv-row',
'attrs' => [
'content' => $widget->display_title(),
],
'innerBlocks' => parse_blocks( $text ),
'innerHTML' => '<div class="wp-block-cac-cv-row">' . $header_fragment . '<div class="cac-cv-row-content"></div></div>',
'innerContent' => [
'<div class="wp-block-cac-cv-row">' . $header_fragment . '<div class="cac-cv-row-content">',
null,
'</div></div>',
],
];
$cv_rows[] = $cv_row_block;
break;
}
}
$cv_rows_block_inner_content = [ '<div class="wp-block-cac-cv-rows">' ];
$cv_rows_block_inner_content = array_merge( $cv_rows_block_inner_content, array_fill( 0, count( $cv_rows ), null ) );
$cv_rows_block_inner_content[] = '</div>';
$cv_rows_block = [
'blockName' => 'cac/cv-rows',
'attrs' => [],
'innerBlocks' => $cv_rows,
'innerHTML' => '<div class="wp-block-cac-cv-rows"></div>',
'innerContent' => $cv_rows_block_inner_content,
];
return $cv_rows_block;
}
global $wpdb;
$user_ids = $wpdb->get_col( "SELECT ID FROM {$wpdb->users} WHERE deleted = 0 LIMIT 20" );
foreach ( $user_ids as $user_id ) {
$user_cv = [
get_cv_top_block( $user_id ),
get_cv_rows_block( $user_id ),
];
echo "\n";
print_r( serialize_blocks( $user_cv ) );
echo "\n";
die;
}
@jeremyfelt
Copy link

We'll need to rework the profile image a bit. I went with an attribute rather than an attached media item so that profile images didn't appear in the media library.

This is now <!-- wp:cac/cv-profile-image /--> with an imageURL attribute containing the full string.

Also, the cac/cv-profile-navigation block is similar to 'cac/cv-shortlinkandcac/cv-last-activein that auserID` attribute is used to help get around funky Gutenberg stuff.

@boonebgorges
Copy link
Author

Thanks for reviewing, @jeremyfelt ! I've made some changes to address both items - see https://gist.github.com/boonebgorges/60467a056ec9f26a044ace3e4d52fdc9/revisions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment