<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[6268] sites/trunk/wordcamp.org/public_html/wp-content/plugins/wc-post-types: WordCamp Post Types: Add ability for attendees to "favorite" sessions.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="http://meta.trac.wordpress.org/changeset/6268">6268</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"http://meta.trac.wordpress.org/changeset/6268","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>iandunn</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2017-12-13 22:42:47 +0000 (Wed, 13 Dec 2017)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>WordCamp Post Types: Add ability for attendees to "favorite" sessions.

This contains the code for emailing the saved sessions to yourself, but the UI for that is temporarily disabled pending a discussion with the Systems team about potential abuse mitigations.

See <a href="http://meta.trac.wordpress.org/ticket/2733">#2733</a>
Props egmanekki</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswcposttypescssshortcodescss">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wc-post-types/css/shortcodes.css</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswcposttypesincrestapiphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wc-post-types/inc/rest-api.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswcposttypeswcposttypesphp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wc-post-types/wc-post-types.php</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswcposttypesincfavoritescheduleshortcodephp">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wc-post-types/inc/favorite-schedule-shortcode.php</a></li>
<li><a href="#sitestrunkwordcamporgpublic_htmlwpcontentpluginswcposttypesjsfavouritesessionsjs">sites/trunk/wordcamp.org/public_html/wp-content/plugins/wc-post-types/js/favourite-sessions.js</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswcposttypescssshortcodescss"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wc-post-types/css/shortcodes.css</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wc-post-types/css/shortcodes.css  2017-12-13 21:58:47 UTC (rev 6267)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wc-post-types/css/shortcodes.css    2017-12-13 22:42:47 UTC (rev 6268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,6 +1,179 @@
</span><span class="cx" style="display: block; padding: 0 10px"> /*
</span><span class="cx" style="display: block; padding: 0 10px">  * [schedule]
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ .wcb-favourite-session {
+       background: #e0f8ff;
+}
+
+.wcpt-schedule td {
+       vertical-align: top;
+}
+
+.wcpt-schedule div.wcb-session-favourite-icon {
+       float: right;
+       width: 35px;
+       text-align: center;
+}
+
+.wcpt-schedule .dashicons {
+       position: relative;
+       box-sizing: content-box;
+       width: 25px;
+       height: 25px;
+       overflow: hidden;
+       white-space: nowrap;
+       font-size: 16px;
+       line-height: 1;
+       cursor: pointer;
+}
+
+.wcpt-schedule .dashicons:before {
+       margin-right: 0px;
+}
+
+.wcpt-schedule .dashicons:after {
+       display: block;
+       font-size: 9px;
+       color: #999;
+       text-align: right;
+}
+
+
+div.wcb-session-favourite-icon a.fav-session-button {
+       color: #e8e8e8;
+       text-decoration: none;
+}
+
+
+div.wcb-session-favourite-icon a.fav-session-button:hover,
+#content a.fav-session-button:hover {
+       color: #fff689;
+       text-decoration: none;
+}
+
+
+td.wcb-favourite-session a.fav-session-button {
+       color: #fff689;
+}
+
+.fav-session-email-wait-spinner {
+       display: none;
+       border: 2px solid #f3f3f3;
+       border-radius: 50%;
+       border-top: 2px solid #777;
+       width: 16px;
+       height: 16px;
+       margin: 10px auto;
+       -webkit-animation: spin 2s linear infinite;
+       animation: spin 2s linear infinite;
+}
+
+@-webkit-keyframes spin {
+       0% { -webkit-transform: rotate( 0deg ); }
+       100% { -webkit-transform: rotate( 360deg ); }
+}
+
+@keyframes spin {
+       0% { transform: rotate( 0deg ); }
+       100% { transform: rotate( 360deg ); }
+}
+
+
+/*
+ * CSS slide for email form for favourite sessions
+ */
+.fav-session-email-form-hide {
+       overflow: hidden;
+       max-height: 0;
+       padding-top: 0;
+       padding-bottom: 0;
+       margin-top: 0;
+       margin-bottom: 0;
+       -moz-transition-duration: 0.5s;
+       -webkit-transition-duration: 0.5s;
+       -o-transition-duration: 0.5s;
+       transition-duration: 0.5s;
+       -moz-transition-timing-function: cubic-bezier( 0, 1, 0.5, 1 );
+       -webkit-transition-timing-function: cubic-bezier( 0, 1, 0.5, 1 );
+       -o-transition-timing-function: cubic-bezier( 0, 1, 0.5, 1 );
+       transition-timing-function: cubic-bezier( 0, 1, 0.5, 1 );
+}
+
+.fav-session-email-form-show {
+       -moz-transition-duration: 0.5s;
+       -webkit-transition-duration: 0.5s;
+       -o-transition-duration: 0.5s;
+       transition-duration: 0.5s;
+       -moz-transition-timing-function: ease-in;
+       -webkit-transition-timing-function: ease-in;
+       -o-transition-timing-function: ease-in;
+       transition-timing-function: ease-in;
+       max-height: 1000px;
+       overflow: hidden;
+}
+
+.show-email-form {
+       display: none;
+       position: fixed;
+       bottom: 20px;
+       right: 100px;
+       padding: 5px 8px 1px 7px;
+       border: 1px solid #a1a1a1;
+       background: #dddddd;
+       border-radius: 6px;
+       color: #a1a1a1;
+       z-index: 9999;
+}
+
+.email-form {
+       position: fixed;
+       bottom: 50px;
+       right: 100px;
+       width: 200px;
+       background: #dcdcdc;
+       font-size: 12px;
+       z-index: 9999;
+       border-radius: 6px;
+}
+
+#fav-session-email-form {
+       margin: 10px;
+}
+
+#fav-sessions-email-address {
+       width: 100%;
+       margin-bottom: 5px;
+       padding: 1px 5px;
+}
+
+.fav-session-email-result {
+       display: none;
+       margin: 10px;
+}
+
+.fav-sessions-form {
+       display: none;
+}
+
+.show-email-form,
+.entry-content a.show-email-form {
+       color: #a1a1a1;
+       text-decoration: none;
+}
+
+.show-email-form:hover,
+.entry-content a.show-email-form:hover,
+#content a.show-email-form:hover {
+       color: #555;
+       text-decoration: none;
+}
+
+.show-email-form .dashicons-star-filled {
+       font-size: 14px;
+       padding-top: 2px;
+       width: 14px;
+}
+
</ins><span class="cx" style="display: block; padding: 0 10px"> @media screen and ( max-width: 700px ) {
</span><span class="cx" style="display: block; padding: 0 10px">        .wcpt-schedule {
</span><span class="cx" style="display: block; padding: 0 10px">                border: none;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -72,6 +245,12 @@
</span><span class="cx" style="display: block; padding: 0 10px">        span.wcpt-session-speakers a {
</span><span class="cx" style="display: block; padding: 0 10px">                color: #21759b;
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       .show-email-form,
+       .email-form {
+               right: 20px;
+       }
+
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /*
</span></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswcposttypesincfavoritescheduleshortcodephp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wc-post-types/inc/favorite-schedule-shortcode.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wc-post-types/inc/favorite-schedule-shortcode.php                         (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wc-post-types/inc/favorite-schedule-shortcode.php   2017-12-13 22:42:47 UTC (rev 6268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,432 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+
+/**
+ * [schedule] shortcode building blocks and favourite session picker support.
+ */
+
+defined( 'WPINC' ) || die();
+
+/**
+ * Return an associative array of term_id -> term object mapping for all selected tracks.
+ *
+ * In case of 'all' is used as a value for $selected_tracks, information for all available tracks
+ * gets returned.
+ *
+ * @param string $selected_tracks Comma-separated list of tracks to display or 'all'.
+ *
+ * @return array Associative array of terms with term_id as the key.
+ */
+function get_schedule_tracks( $selected_tracks ) {
+       $tracks = array();
+       if ( 'all' === $selected_tracks ) {
+               // Include all tracks.
+               $tracks = get_terms( 'wcb_track' );
+       } else {
+               // Loop through given tracks and look for terms.
+               $terms = array_map( 'trim', explode( ',', $selected_tracks ) );
+
+               foreach ( $terms as $term_slug ) {
+                       $term = get_term_by( 'slug', $term_slug, 'wcb_track' );
+                       if ( $term ) {
+                               $tracks[ $term->term_id ] = $term;
+                       }
+               }
+       }
+
+       return $tracks;
+}
+
+/**
+ * Return a time-sorted associative array mapping timestamp -> track_id -> session id.
+ *
+ * @param string $schedule_date               Date for which the sessions should be retrieved.
+ * @param bool   $tracks_explicitly_specified True if tracks were explicitly specified in the shortcode,
+ *                                            false otherwise.
+ * @param array  $tracks                      Array of terms for tracks from get_schedule_tracks().
+ *
+ * @return array Associative array of session ids by time and track.
+ */
+function get_schedule_sessions( $schedule_date, $tracks_explicitly_specified, $tracks ) {
+       $query_args = array(
+               'post_type'      => 'wcb_session',
+               'posts_per_page' => - 1,
+               'meta_query'     => array(
+                       'relation' => 'AND',
+                       array(
+                               'key'     => '_wcpt_session_time',
+                               'compare' => 'EXISTS',
+                       ),
+               ),
+       );
+
+       if ( $schedule_date && strtotime( $schedule_date ) ) {
+               $query_args['meta_query'][] = array(
+                       'key'     => '_wcpt_session_time',
+                       'value'   => array(
+                               strtotime( $schedule_date ),
+                               strtotime( $schedule_date . ' +1 day' ),
+                       ),
+                       'compare' => 'BETWEEN',
+                       'type'    => 'NUMERIC',
+               );
+       }
+
+       if ( $tracks_explicitly_specified ) {
+               // If tracks were provided, restrict the lookup in WP_Query.
+               if ( ! empty( $tracks ) ) {
+                       $query_args['tax_query'][] = array(
+                               'taxonomy' => 'wcb_track',
+                               'field'    => 'id',
+                               'terms'    => array_values( wp_list_pluck( $tracks, 'term_id' ) ),
+                       );
+               }
+       }
+
+       // Loop through all sessions and assign them into the formatted
+       // $sessions array: $sessions[ $time ][ $track ] = $session_id
+       // Use 0 as the track ID if no tracks exist.
+       $sessions       = array();
+       $sessions_query = new WP_Query( $query_args );
+
+       foreach ( $sessions_query->posts as $session ) {
+               $time  = absint( get_post_meta( $session->ID, '_wcpt_session_time', true ) );
+               $terms = get_the_terms( $session->ID, 'wcb_track' );
+
+               if ( ! isset( $sessions[ $time ] ) ) {
+                       $sessions[ $time ] = array();
+               }
+
+               if ( empty( $terms ) ) {
+                       $sessions[ $time ][0] = $session->ID;
+               } else {
+                       foreach ( $terms as $track ) {
+                               $sessions[ $time ][ $track->term_id ] = $session->ID;
+                       }
+               }
+       }
+
+       // Sort all sessions by their key (timestamp).
+       ksort( $sessions );
+
+       return $sessions;
+}
+
+/**
+ * Return an array of columns identified by term ids to be used for schedule table.
+ *
+ * @param array $tracks                      Array of terms for tracks from get_schedule_tracks().
+ * @param array $sessions                    Array of sessions from get_schedule_sessions().
+ * @param bool  $tracks_explicitly_specified True if tracks were explicitly specified in the shortcode,
+ *                                           false otherwise.
+ *
+ * @return array Array of columns identified by term ids.
+ */
+function get_schedule_columns( $tracks, $sessions, $tracks_explicitly_specified ) {
+       $columns = array();
+
+       // Use tracks to form the columns.
+       if ( $tracks ) {
+               foreach ( $tracks as $track ) {
+                       $columns[ $track->term_id ] = $track->term_id;
+               }
+       } else {
+               $columns[0] = 0;
+       }
+
+       // Remove empty columns unless tracks have been explicitly specified.
+       if ( ! $tracks_explicitly_specified ) {
+               $used_terms = array();
+
+               foreach ( $sessions as $time => $entry ) {
+                       if ( is_array( $entry ) ) {
+                               foreach ( $entry as $term_id => $session_id ) {
+                                       $used_terms[ $term_id ] = $term_id;
+                               }
+                       }
+               }
+
+               $columns = array_intersect( $columns, $used_terms );
+               unset( $used_terms );
+       }
+
+       return $columns;
+}
+
+/**
+ * Update and preprocess input attributes for [schedule] shortcode.
+ *
+ * @param array $attr Array of attributes from shortcode.
+ *
+ * @return array Array of attributes, after preprocessing.
+ */
+function preprocess_schedule_attributes( $attr ) {
+       $attr = shortcode_atts(
+               array(
+                       'date'         => null,
+                       'tracks'       => 'all',
+                       'speaker_link' => 'anchor',    // anchor|wporg|permalink|none
+                       'session_link' => 'permalink', // permalink|anchor|none
+               ), $attr
+       );
+
+       foreach ( array( 'tracks', 'speaker_link', 'session_link' ) as $key_for_case_sensitive_value ) {
+               $attr[ $key_for_case_sensitive_value ] = strtolower( $attr[ $key_for_case_sensitive_value ] );
+       }
+
+       if ( ! in_array( $attr['speaker_link'], array( 'anchor', 'wporg', 'permalink', 'none' ), true ) ) {
+               $attr['speaker_link'] = 'anchor';
+       }
+
+       if ( ! in_array( $attr['session_link'], array( 'permalink', 'anchor', 'none' ), true ) ) {
+               $attr['session_link'] = 'permalink';
+       }
+
+       return $attr;
+}
+
+/**
+ * Return plain text list of sessions marked as favourite sessions.
+ *
+ * Format of each list item:
+ * Time of session | Session title [by Speaker] | Track name(s).
+ *
+ * @param array $sessions_rev        Array of sessions with reversed subarray track_id->session_id.
+ * @param array $fav_sessions_lookup Mapping session _id -> 1 for favourite sessions.
+ *
+ * @return string List of sessions.
+ */
+function generate_plaintext_fav_sessions( $sessions_rev, $fav_sessions_lookup ) {
+       $sessions_text = '';
+
+       // timestamp -> session_id -> track_id.
+       foreach ( $sessions_rev as $timestamp => $sessions_at_time ) {
+               foreach ( $sessions_at_time as $session_id => $track_ids ) {
+                       // Skip sessions which are not marked favourite.
+                       if ( ! isset( $fav_sessions_lookup[ $session_id ] ) ) {
+                               continue;
+                       }
+
+                       $session              = get_post( $session_id );
+                       $session_title        = apply_filters( 'the_title', $session->post_title );
+                       $session_tracks       = get_the_terms( $session_id, 'wcb_track' );
+                       $session_track_titles = is_array( $session_tracks ) ? implode( ', ', wp_list_pluck( $session_tracks, 'name' ) ) : '';
+
+                       $speakers     = array();
+                       $speakers_ids = array_map( 'absint', (array) get_post_meta( $session_id, '_wcpt_speaker_id' ) );
+                       if ( ! empty( $speakers_ids ) ) {
+                               $speakers = get_posts( array(
+                                       'post_type'      => 'wcb_speaker',
+                                       'posts_per_page' => - 1,
+                                       'post__in'       => $speakers_ids,
+                               ) );
+                       }
+
+                       $speakers_names = array();
+                       foreach ( $speakers as $speaker ) {
+                               $speaker_name     = apply_filters( 'the_title', $speaker->post_title );
+                               $speakers_names[] = $speaker_name;
+                       }
+
+                       // Line format: Time of session | Session title [by Speaker] | Track name(s).
+                       $sessions_text .= date( get_option( 'time_format' ), $timestamp );
+                       $sessions_text .= ' | ';
+                       $sessions_text .= $session_title;
+                       if ( count( $speakers_names ) > 0 ) {
+                               $sessions_text .= _x( ' by ', 'Speaker for the session', 'wordcamporg' ) . implode( ', ', $speakers_names );
+                       }
+                       $sessions_text .= ' | ';
+                       $sessions_text .= $session_track_titles;
+                       $sessions_text .= "\n";
+               }
+       }
+
+       return $sessions_text;
+
+}
+
+/**
+ * Return array of dates for which there are sessions in the provided array.
+ *
+ * @param array  $sessions    Array of sessions from get_schedule_sessions().
+ * @param string $date_format Date format string (same format as php).
+ *
+ * @return array Array of dates for WordCamp formatted according to date_format string.
+ */
+function get_sessions_dates( $sessions, $date_format ) {
+       $session_timestamps = array_keys( $sessions );
+
+       $session_dates = array_map(
+               function( $timestamp ) use ( $date_format ) {
+                       return date( $date_format, $timestamp );
+               },
+               $session_timestamps
+       );
+
+       return array_unique( $session_dates );
+}
+
+/**
+ * Return true if any of the sessions from $session_rev is in $fav_session_ids,
+ * false otherwise.
+ *
+ * @param array $fav_session_ids Array with favourite sessions as keys.
+ * @param array $sessions_rev    Array of sessions from flip_sessions_subarrays().
+ *
+ * @return bool true if there is any intersection, false otherwise.
+ */
+function includes_fav_session( $fav_session_ids, $sessions_rev ) {
+       foreach ( $sessions_rev as $timestamp => $session_id_subarray ) {
+               foreach ( $session_id_subarray as $session_id => $_ ) {
+                       if ( isset( $fav_session_ids[ $session_id ] ) ) {
+                               return true;
+                       }
+               }
+       }
+
+       return false;
+}
+
+/**
+ * Return array of sessions with reverted subarrays, i.e. transformed from
+ * timestamp -> track_id -> session_id into
+ * timestamp -> session_id -> [track_id1, track_id2, ...]
+ *
+ * @param array $sessions An array of sessions from get_schedule_sessions().
+ *
+ * @return array Array with format timestamp -> session_id -> [track_id1, track_id2, ...].
+ */
+function flip_sessions_subarrays( $sessions ) {
+       $sessions_reversed = array();
+
+       foreach ( $sessions as $timestamp => $sessions_at_time ) {
+               foreach ( $sessions_at_time as $track_id => $session_id ) {
+                       $sessions_reversed[ $timestamp ][ $session_id ][] = $track_id;
+               }
+       }
+
+       return $sessions_reversed;
+}
+
+/**
+ * Return plain text email message body for sharing favourite sessions email.
+ *
+ * @param string $wordcamp_name       WordCamp name to be used in the email.
+ * @param array  $fav_sessions_lookup Mapping session _id -> 1 for favourite sessions.
+ *
+ * @return string                     Plain text body of the email.
+ */
+function generate_email_body( $wordcamp_name, $fav_sessions_lookup ) {
+       $date_format                 = get_option( 'date_format' );
+       $tracks                      = get_schedule_tracks( 'all' );
+       $tracks_explicitly_specified = false; // include all tracks in the email.
+       $sessions                    = get_schedule_sessions( null, $tracks_explicitly_specified, $tracks );
+       $sessions_dates              = get_sessions_dates( $sessions, $date_format );
+
+       // Convert timestamp -> track_id -> session_id to timestamp -> session_id -> [track_id1, ...].
+       $sessions_reversed = flip_sessions_subarrays( $sessions );
+
+       $email_message = $wordcamp_name . "\n\n";
+
+       // Create list of sessions for each day.
+       foreach ( $sessions_dates as $current_day ) {
+               // Filter only the sessions for the 'current' day.
+               $sessions_for_current_day = array_filter(
+                       $sessions_reversed,
+                       function( $date_ ) use ( $current_day, $date_format ) {
+                               return date( $date_format, $date_ ) === $current_day;
+                       },
+                       ARRAY_FILTER_USE_KEY
+               );
+
+               $email_message .= $current_day . "\n";
+
+               // Skip days when there's no session marked as favourite.
+               if ( ! includes_fav_session( $fav_sessions_lookup, $sessions_for_current_day ) ) {
+                       $email_message .= "\n";
+                       continue;
+               }
+
+               $email_message .= generate_plaintext_fav_sessions( $sessions_for_current_day, $fav_sessions_lookup );
+               $email_message .= "\n\n";
+       }
+
+       return $email_message;
+}
+
+/**
+ * Return true if the email favourite sessions feature should be disabled,
+ * false otherwise.
+ *
+ * Kill switch for sharing schedule over email -- both for REST API endpoint and UI
+ * in [schedule] shortcode.
+ *
+ * @return bool true if email functionality should be disabled, false otherwise.
+ */
+function email_fav_sessions_disabled() {
+       return true;    // @todo enable after finish discussing abuse mitigation
+}
+
+/**
+ * Send favourite sessions email to address specified in the REST request.
+ *
+ * REST API handler for 'wc-post-types/v1/email-fav-sessions' endpoint.
+ *
+ * @param WP_REST_Request $request REST API Request object.
+ *
+ * @return WP_REST_Response|WP_Error
+ */
+function send_favourite_sessions_email( WP_REST_Request $request ) {
+       // There's no need to check intention or authorization, since this is meant to be available to
+       // unauthenticated visitors.
+
+       if ( email_fav_sessions_disabled() ) {
+               return new WP_REST_Response(
+                       array(
+                               'message' => esc_html__( 'Email functionality disabled.', 'wordcamporg' ),
+                       ), 200
+               );
+       }
+
+       $params = $request->get_params();
+       // Input sanitized by REST controller.
+       $email_address = $params['email-address'];
+       $fav_sessions  = $params['session-list'];
+
+       // Don't send the email if no sessions were marked as favourite.
+       if ( count( explode( ',', $fav_sessions ) ) === 0 ) {
+               return new WP_Error(
+                       'fav_sessions_no_sessions',
+                       esc_html__( 'No sessions selected.', 'wordcamporg' ),
+                       array(
+                               'status' => 400,
+                       )
+               );
+       }
+
+       $fav_sessions_lookup = array_fill_keys( explode( ',', $fav_sessions ), 1 );
+
+       $wordcamp_name = get_wordcamp_name();
+
+       $headers[] = 'From: ' . $wordcamp_name . ' <do-not-reply@wordcamp.org>';
+       $headers[] = 'Content-Type: text/plain; charset=' . get_bloginfo( 'charset' );
+
+       $subject = sprintf( __( 'My favorite sessions for %s', 'wordcamporg' ), $wordcamp_name );
+       $message = generate_email_body( $wordcamp_name, $fav_sessions_lookup );
+
+       if ( wp_mail( $email_address, $subject, $message, $headers ) ) {
+               return new WP_REST_Response(
+                       array(
+                               'message' => esc_html__( 'Email sent successfully to ', 'wordcamporg' ) . " $email_address.",
+                       ), 200
+               );
+       }
+
+       // Email was not sent successfully.
+       return new WP_Error(
+               'fav_sessions_email_failed',
+               esc_html__( 'Favourite sessions email failed.', 'wordcamporg' ),
+               array(
+                       'status' => 500,
+               )
+       );
+}
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswcposttypesincrestapiphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wc-post-types/inc/rest-api.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wc-post-types/inc/rest-api.php    2017-12-13 21:58:47 UTC (rev 6267)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wc-post-types/inc/rest-api.php      2017-12-13 22:42:47 UTC (rev 6268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -6,8 +6,12 @@
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> namespace WordCamp\Post_Types\REST_API;
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+use WP_Rest_Server;
+
</ins><span class="cx" style="display: block; padding: 0 10px"> defined( 'WPINC' ) || die();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+require_once( 'favorite-schedule-shortcode.php' );
+
</ins><span class="cx" style="display: block; padding: 0 10px"> /**
</span><span class="cx" style="display: block; padding: 0 10px">  * Add non-sensitive meta fields to the speaker/session REST API endpoints
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -111,9 +115,59 @@
</span><span class="cx" style="display: block; padding: 0 10px">        } // End if().
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
</ins><span class="cx" style="display: block; padding: 0 10px"> add_action( 'rest_api_init', __NAMESPACE__ . '\register_additional_rest_fields' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
</ins><span class="cx" style="display: block; padding: 0 10px"> /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Register route for sending schedule of favourite sessions via e-mail.
+ *
+ * This can be disabled in email_fav_sessions_disabled() from favorite-schedule-shortcode.php.
+ *
+ * @return void
+ */
+function register_fav_sessions_email(){
+       register_rest_route(
+               'wc-post-types/v1',     // REST namespace + API version
+               '/email-fav-sessions/', // URL slug
+               array(
+                       'methods'  => WP_REST_Server::CREATABLE,
+                       'callback' => 'send_favourite_sessions_email',
+                       'args'     => array(
+                               'email-address' => array(
+                                       'required' => true,
+                                       'validate_callback' => function( $value, $request, $param ) {
+                                               return is_email( $value );
+                                       },
+                                       'sanitize_callback' => function( $value, $request, $param ) {
+                                               return sanitize_email( $value );
+                                       },
+                               ),
+
+                               'session-list' => array(
+                                       'required' => true,
+                                       'validate_callback' => function( $value, $request, $param ) {
+                                               $session_ids = explode( ',', $value );
+                                               $session_count = count( $session_ids );
+                                               for ( $i = 0; $i < $session_count; $i++ ) {
+                                                       if ( ! is_numeric( $session_ids[ $i ] ) ) {
+                                                               return false;
+                                                       }
+                                               }
+                                               return true;
+                                       },
+                                       'sanitize_callback' => function( $value, $request, $param ) {
+                                               $session_ids = explode( ',', $value );
+                                               return implode( ',', array_filter( $session_ids, 'is_numeric' ) );
+                                       },
+                               ),
+                       )
+               )
+       );
+}
+add_action( 'rest_api_init', __NAMESPACE__ . '\register_fav_sessions_email' );
+
+/**
</ins><span class="cx" style="display: block; padding: 0 10px">  * Link all sessions to the speaker in the `speakers` API endpoint
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * This allows clients to request a speaker and get all their sessions embedded in the response, avoiding
</span></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswcposttypesjsfavouritesessionsjs"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wc-post-types/js/favourite-sessions.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wc-post-types/js/favourite-sessions.js                            (rev 0)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wc-post-types/js/favourite-sessions.js      2017-12-13 22:42:47 UTC (rev 6268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,167 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+jQuery( document ).ready( function ( $ ) {
+       var FavSessions = {
+               favSessKey: 'favourite_sessions',
+
+               get: function () {
+                       var favSessions = JSON.parse( localStorage.getItem( this.favSessKey ) );
+
+                       if ( ! favSessions ) {
+                               favSessions = {};
+                       }
+
+                       return favSessions;
+               },
+
+               toggleSession: function ( sessionId ) {
+                       var favSessions = this.get();
+
+                       if ( favSessions.hasOwnProperty( sessionId ) ) {
+                               delete favSessions[ sessionId ];
+                       } else {
+                               favSessions[ sessionId ] = true;
+                       }
+
+                       localStorage.setItem( this.favSessKey, JSON.stringify( favSessions ) );
+               },
+       };
+
+       function switchCellAppearance( sessionId ) {
+               // (Un)highlight schedule table cell in case a session is (un)marked as favourite.
+               var sessionSelector = '[data-session-id=\'' + sessionId + '\']';
+               var tdElements = document.querySelectorAll( sessionSelector );
+
+               for ( var i = 0; i < tdElements.length; i ++ ) {
+                       tdElements[ i ].classList.toggle( 'wcb-favourite-session' );
+               }
+       }
+
+       function switchEmailFavButton() {
+               var favSessions = FavSessions.get();
+
+               // Display email form only if there are any selected sessions.
+               if ( Object.keys( favSessions ).length > 0 ) {
+                       $( '.show-email-form' ).show();
+               } else {
+                       $( '.show-email-form' ).hide();
+               }
+       }
+
+       function switchSessionFavourite( sessionId ) {
+               FavSessions.toggleSession( sessionId );
+               switchCellAppearance( sessionId );
+               switchEmailFavButton();
+       }
+
+       function initFavouriteSessions() {
+               var favSessions = FavSessions.get();
+
+               if ( favSessions === {} ) {
+                       return;
+               }
+
+               // Highlight favourite sessions in table.
+               var sessionIds = Object.keys( favSessions );
+
+               for ( var i = 0; i < sessionIds.length; i ++ ) {
+                       var sessionId = sessionIds[ i ];
+
+                       if ( favSessions[ sessionId ] === true ) {
+                               switchCellAppearance( sessionId );
+                       }
+               }
+
+               switchEmailFavButton();
+       }
+
+       function hideSpinnerShowResult( message ) {
+               var fadeInDelay = 300;
+               $( '.fav-session-email-wait-spinner' ).fadeOut( fadeInDelay );
+
+               setTimeout( function () {
+                       $( '.fav-session-email-result' ).html( message );
+                       $( '.fav-session-email-result' ).fadeIn();
+               }, fadeInDelay );
+       }
+
+       function hideFormShowSpinner() {
+               var fadeInDelay = 300;
+
+               $( '#fav-session-email-form' ).fadeOut( fadeInDelay );
+
+               setTimeout( function () {
+                       $( '.fav-session-email-wait-spinner' ).fadeIn();
+               }, fadeInDelay );
+       }
+
+       $( '.show-email-form' ).click( function ( event ) {
+               event.preventDefault();
+
+               // Slide the slider.
+               $( '.email-form' ).toggleClass( 'fav-session-email-form-hide' ).toggleClass( 'fav-session-email-form-show' );
+
+               // After the animation finishes, activate the form again and hide the previous result.
+               setTimeout( function () {
+                       // Clear previous email result.
+                       $( '.fav-session-email-result' ).html( '' );
+
+                       // Show form div & clear email address.
+                       $( '#fav-session-email-form' ).show();
+                       $( '#fav-sessions-email-address' ).val( '' );
+               }, 500 );
+
+               return false;
+       } );
+
+       $( '.fav-session-button' ).click( function ( event ) {
+               event.preventDefault();
+
+               var elem = $( this );
+               var sessionId = elem.parent().parent().data( 'session-id' );
+               switchSessionFavourite( sessionId );
+
+               return false;
+       } );
+
+       $( '#fav-sessions-form' ).on( 'submit', function ( event ) {
+               event.preventDefault();
+               hideFormShowSpinner();
+               var favSessions = FavSessions.get();
+               favSessions = Object.keys( favSessions ).toString();
+
+               // Get email from the input.
+               var emailAddress = '';
+               if ( $( '#fav-sessions-email-address' ) ) {
+                       emailAddress = $( '#fav-sessions-email-address' ).val();
+               } else {
+                       return;
+               }
+
+               // Compile data object.
+               var data = {
+                       'email-address': emailAddress,
+                       'session-list': favSessions,
+               };
+
+               $.ajax( {
+                       method: 'POST',
+                       url: favSessionsPhpObject.root + 'wc-post-types/v1/email-fav-sessions',
+                       data: data,
+                       success: function ( response ) {
+                               hideSpinnerShowResult( response.message );
+                       },
+                       fail: function ( response ) {
+                               hideSpinnerShowResult( response.message );
+                       },
+                       error: function ( jqXHR, textStatus, errorThrown ) {
+                               if ( textStatus === 'timeout' ) {
+                                       hideSpinnerShowResult( favSessionsPhpObject.i18n.reqTimeOut );
+                               } else {
+                                       hideSpinnerShowResult( favSessionsPhpObject.i18n.otherError );
+                               }
+                       },
+                       timeout: 5000,
+               } );
+       } );
+
+       initFavouriteSessions();
+} );
</ins></span></pre></div>
<a id="sitestrunkwordcamporgpublic_htmlwpcontentpluginswcposttypeswcposttypesphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordcamp.org/public_html/wp-content/plugins/wc-post-types/wc-post-types.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordcamp.org/public_html/wp-content/plugins/wc-post-types/wc-post-types.php   2017-12-13 21:58:47 UTC (rev 6267)
+++ sites/trunk/wordcamp.org/public_html/wp-content/plugins/wc-post-types/wc-post-types.php     2017-12-13 22:42:47 UTC (rev 6268)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -5,6 +5,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> require( 'inc/back-compat.php' );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+require_once( 'inc/favorite-schedule-shortcode.php' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> class WordCamp_Post_Types_Plugin {
</span><span class="cx" style="display: block; padding: 0 10px">        protected $wcpt_permalinks;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -513,120 +514,15 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @todo cleanup
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        function shortcode_schedule( $attr, $content ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $attr = shortcode_atts( array(
-                       'date'         => null,
-                       'tracks'       => 'all',
-                       'speaker_link' => 'anchor', // anchor|wporg|permalink|none
-                       'session_link' => 'permalink', // permalink|anchor|none
-               ), $attr );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $this->enqueue_schedule_shortcode_dependencies();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                foreach ( array( 'tracks', 'speaker_link', 'session_link' ) as $key_for_case_sensitive_value ) {
-                       $attr[ $key_for_case_sensitive_value ] = strtolower( $attr[ $key_for_case_sensitive_value ] );
-               }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $attr                        = preprocess_schedule_attributes( $attr );
+               $tracks                      = get_schedule_tracks( $attr['tracks'] );
+               $tracks_explicitly_specified = 'all' !== $attr['tracks'];
+               $sessions                    = get_schedule_sessions( $attr['date'], $tracks_explicitly_specified, $tracks );
+               $columns                     = get_schedule_columns( $tracks, $sessions, $tracks_explicitly_specified );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( ! in_array( $attr['speaker_link'], array( 'anchor', 'wporg', 'permalink', 'none' ) ) )
-                       $attr['speaker_link'] = 'anchor';
-
-               if ( ! in_array( $attr['session_link'], array( 'permalink', 'anchor', 'none' ) ) )
-                       $attr['session_link'] = 'permalink';
-
-               $columns = array();
-               $tracks = array();
-
-               $query_args = array(
-                       'post_type'      => 'wcb_session',
-                       'posts_per_page' => -1,
-                       'meta_query'     => array(
-                               'relation'   => 'AND',
-                               array(
-                                       'key'     => '_wcpt_session_time',
-                                       'compare' => 'EXISTS',
-                               ),
-                       ),
-               );
-
-               if ( 'all' == $attr['tracks'] ) {
-                       // Include all tracks.
-                       $tracks = get_terms( 'wcb_track' );
-               } else {
-                       // Loop through given tracks and look for terms.
-                       $terms = array_map( 'trim', explode( ',', $attr['tracks'] ) );
-                       foreach ( $terms as $term_slug ) {
-                               $term = get_term_by( 'slug', $term_slug, 'wcb_track' );
-                               if ( $term )
-                                       $tracks[ $term->term_id ] = $term;
-                       }
-
-                       // If tracks were provided, restrict the lookup in WP_Query.
-                       if ( ! empty( $tracks ) ) {
-                               $query_args['tax_query'][] = array(
-                                       'taxonomy' => 'wcb_track',
-                                       'field'    => 'id',
-                                       'terms'    => array_values( wp_list_pluck( $tracks, 'term_id' ) ),
-                               );
-                       }
-               }
-
-               if ( $attr['date'] && strtotime( $attr['date'] ) ) {
-                       $query_args['meta_query'][] = array(
-                               'key'   => '_wcpt_session_time',
-                               'value' => array(
-                                       strtotime( $attr['date'] ),
-                                       strtotime( $attr['date'] . ' +1 day' ),
-                               ),
-                               'compare' => 'BETWEEN',
-                               'type'    => 'NUMERIC',
-                       );
-               }
-
-               // Use tracks to form the columns.
-               if ( $tracks ) {
-                       foreach ( $tracks as $track )
-                               $columns[ $track->term_id ] = $track->term_id;
-               } else {
-                       $columns[ 0 ] = 0;
-               }
-
-               unset( $tracks );
-
-               // Loop through all sessions and assign them into the formatted
-               // $sessions array: $sessions[ $time ][ $track ] = $session_id
-               // Use 0 as the track ID if no tracks exist
-
-               $sessions = array();
-               $sessions_query = new WP_Query( $query_args );
-               foreach ( $sessions_query->posts as $session ) {
-                       $time = absint( get_post_meta( $session->ID, '_wcpt_session_time', true ) );
-                       $tracks = get_the_terms( $session->ID, 'wcb_track' );
-
-                       if ( ! isset( $sessions[ $time ] ) )
-                               $sessions[ $time ] = array();
-
-                       if ( empty( $tracks ) ) {
-                               $sessions[ $time ][ 0 ] = $session->ID;
-                       } else {
-                               foreach ( $tracks as $track )
-                                       $sessions[ $time ][ $track->term_id ] = $session->ID;
-                       }
-               }
-
-               // Sort all sessions by their key (timestamp).
-               ksort( $sessions );
-
-               // Remove empty columns unless tracks have been explicitly specified
-               if ( 'all' == $attr['tracks'] ) {
-                       $used_terms = array();
-
-                       foreach ( $sessions as $time => $entry )
-                               if ( is_array( $entry ) )
-                                       foreach ( $entry as $term_id => $session_id )
-                                               $used_terms[ $term_id ] = $term_id;
-
-                       $columns = array_intersect( $columns, $used_terms );
-                       unset( $used_terms );
-               }
-
-               $html = '<table class="wcpt-schedule" border="0">';
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $html  = '<table class="wcpt-schedule" border="0">';
</ins><span class="cx" style="display: block; padding: 0 10px">                 $html .= '<thead>';
</span><span class="cx" style="display: block; padding: 0 10px">                $html .= '<tr>';
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -717,6 +613,11 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                $classes[] = 'wcpt-session-type-' . $session_type;
</span><span class="cx" style="display: block; padding: 0 10px">                                $classes[] = 'wcb-session-' . $session->post_name;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                // Favourite session star-icon.
+                               $content = '<div class="wcb-session-favourite-icon">';
+                               $content .= '<a class="fav-session-button"><span class="dashicons dashicons-star-filled"></span></a></div>';
+                               $content .= '<div class="wcb-session-cell-content">';
+
</ins><span class="cx" style="display: block; padding: 0 10px">                                 // Determine the session title
</span><span class="cx" style="display: block; padding: 0 10px">                                if ( 'permalink' == $attr['session_link'] && 'session' == $session_type )
</span><span class="cx" style="display: block; padding: 0 10px">                                        $session_title_html = sprintf( '<a class="wcpt-session-title" href="%s">%s</a>', esc_url( get_permalink( $session->ID ) ), $session_title );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -725,7 +626,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                else
</span><span class="cx" style="display: block; padding: 0 10px">                                        $session_title_html = sprintf( '<span class="wcpt-session-title">%s</span>', $session_title );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                $content = $session_title_html;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         $content .= $session_title_html;
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                $speakers_names = array();
</span><span class="cx" style="display: block; padding: 0 10px">                                foreach ( $speakers as $speaker ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -748,6 +649,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                if ( count( $speakers_names ) )
</span><span class="cx" style="display: block; padding: 0 10px">                                        $content .= sprintf( ' <span class="wcpt-session-speakers">%s</span>', implode( ', ', $speakers_names ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                // End of cell-content.
+                               $content .= '</div>';
+
</ins><span class="cx" style="display: block; padding: 0 10px">                                 $columns_clone = $columns;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                // If the next element in the table is the same as the current one, use colspan
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -765,7 +669,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        }
</span><span class="cx" style="display: block; padding: 0 10px">                                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                $columns_html .= sprintf( '<td colspan="%d" class="%s" data-track-title="%s">%s</td>', $colspan, esc_attr( implode( ' ', $classes ) ), $session_track_titles, $content );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         $columns_html .= sprintf( '<td colspan="%d" class="%s" data-track-title="%s" data-session-id="%s">%s</td>', $colspan, esc_attr( implode( ' ', $classes ) ), $session_track_titles, esc_attr( $session->ID ), $content );
</ins><span class="cx" style="display: block; padding: 0 10px">                         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        $global_session      = $colspan == count( $columns ) ? ' global-session' : '';
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -779,13 +683,88 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $html .= '</tbody>';
</span><span class="cx" style="display: block; padding: 0 10px">                $html .= '</table>';
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                $html .= $this->fav_session_email_form();
</ins><span class="cx" style="display: block; padding: 0 10px">                 return $html;
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Enqueue style and scripts needed for [schedule] shortcode.
+        */
+       function enqueue_schedule_shortcode_dependencies() {
+               wp_enqueue_style( 'dashicons' );
+
+               wp_enqueue_script(
+                       'favourite-sessions',
+                       plugin_dir_url( __FILE__ ) . 'js/favourite-sessions.js',
+                       array( 'jquery' ),
+                       filemtime( plugin_dir_path( __FILE__ ) . 'js/favourite-sessions.js' ),
+                       true
+               );
+
+               wp_localize_script(
+                       'favourite-sessions',
+                       'favSessionsPhpObject',
+                       array(
+                               'root' => esc_url_raw( rest_url() ),
+                               'i18n' => array(
+                                       'reqTimeOut' => esc_html__( 'Sorry, the email request timed out.', 'wordcamporg' ),
+                                       'otherError' => esc_html__( 'Sorry, the email request failed.',    'wordcamporg' ),
+                               ),
+                       )
+               );
+       }
+
+       /**
+        * Return HTML code for email form used to send/share favourite sessions over email.
+        *
+        * Both form and button/link to show/hide the form can be styled using classes email-form
+        * and show-email-form, respectively.
+        *
+        * @return string HTML code that represents the form to send emails and a link to show and hide it.
+        */
+       function fav_session_email_form() {
+               static $email_form_count = 0;
+
+               // Skip email form if it is disabled or it was already added to document.
+               if ( email_fav_sessions_disabled() || $email_form_count !== 0 ) {
+                       return '';
+               }
+
+               ob_start();
+               ?>
+
+               <div class="email-form fav-session-email-form-hide">
+                       <div id="fav-session-email-form">
+                               <?php esc_html_e( 'Send me my favorite sessions:', 'wordcamporg' ); ?>
+
+                               <form id="fav-sessions-form">
+                                       <input type="text" name="email_address" id="fav-sessions-email-address" placeholder="my@email.com" />
+                                       <input type="submit" value="<?php esc_attr_e( 'Send', 'wordcamporg' ); ?>" />
+                               </form>
+                       </div>
+                       <div class="fav-session-email-wait-spinner"></div>
+                       <div class="fav-session-email-result"></div>
+               </div>
+
+               <a class="show-email-form" href="javascript:">
+                       <span class="dashicons dashicons-star-filled"></span>
+                       <span class="dashicons dashicons-email-alt"></span>
+               </a>
+
+               <?php
+               $email_form = ob_end_flush();
+
+               $email_form_count++;
+
+               return $email_form;
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Returns a speaker's WordPress.org profile url (if username set)
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param $speaker_id int The speaker's post id.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         *
+        * @return NULL|string
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><span class="cx" style="display: block; padding: 0 10px">        function get_speaker_wporg_permalink( $speaker_id ) {
</span><span class="cx" style="display: block; padding: 0 10px">                $post = get_post( $speaker_id );
</span></span></pre>
</div>
</div>

</body>
</html>