Making WordPress.org

Ticket #2955: 2955.1.patch

File 2955.1.patch, 14.4 KB (added by pierlo, 5 years ago)
  • new file wordpress.org/public_html/wp-content/plugins/wporg-meeting-ical/class-ical-generator.php

    diff --git wordpress.org/public_html/wp-content/plugins/wporg-meeting-ical/class-ical-generator.php wordpress.org/public_html/wp-content/plugins/wporg-meeting-ical/class-ical-generator.php
    new file mode 100644
    index 000000000..5e6540833
    - +  
     1<?php
     2
     3
     4class Meeting_ICAL_Generator
     5{
     6
     7    const NEWLINE = "\r\n";
     8
     9    /**
     10     * Generate an iCalendar for the given set of meetings.
     11     *
     12     * @param WP_Post[] $posts
     13     * @return string
     14     */
     15    public function generate($posts)
     16    {
     17        $ical = 'BEGIN:VCALENDAR' . self::NEWLINE;
     18        $ical .= 'VERSION:2.0' . self::NEWLINE;
     19        $ical .= 'PRODID:-//WPORG Make//Meeting Events Calendar//EN' . self::NEWLINE;
     20        $ical .= 'METHOD:PUBLISH' . self::NEWLINE;
     21        $ical .= 'CALSCALE:GREGORIAN' . self::NEWLINE;
     22
     23        foreach ($posts as $post) {
     24            $ical .= $this->generate_event($post);
     25        }
     26
     27        $ical .= 'END:VCALENDAR';
     28        return $ical;
     29    }
     30
     31    /**
     32     * Generate an event for a meeting.
     33     *
     34     * @param WP_Post $post
     35     * @return string
     36     */
     37    private function generate_event($post)
     38    {
     39        $id = $post->ID;
     40        $title = $post->post_title;
     41        $location = $post->location;
     42        $link = $post->link;
     43        $team = $post->team;
     44        $recurring = $post->recurring;
     45        $sequence = empty($post->sequence) ? 0 : intval($post->sequence);
     46
     47        $start_date = strftime('%Y%m%d', strtotime($post->next_date));
     48        $start_time = strftime('%H%M%S', strtotime($post->time));
     49        $start_date_time = "{$start_date}T{$start_time}Z";
     50
     51        $end_date = $start_date;
     52        $end_time = strftime('%H%M%S', strtotime("{$post->time} +1 hour"));
     53        $end_date_time = "{$end_date}T{$end_time}Z";
     54
     55        $description = '';
     56        $slack_channel = null;
     57
     58        if ($location && preg_match('/^#([-\w]+)$/', trim($location), $match)) {
     59            $slack_channel = '#' . sanitize_title($match[1]);
     60            $location = "{$slack_channel} channel on Slack";
     61        }
     62
     63        if ($link) {
     64            if ($slack_channel) {
     65                $description .= "Slack channel link: https://wordpress.slack.com/messages/{$slack_channel}\\n";
     66            }
     67
     68            $description .= "For more information visit {$link}";
     69        }
     70
     71        $frequency = $this->get_frequency($recurring, $post->next_date, $post->occurrence);
     72
     73        $event = "BEGIN:VEVENT" . self::NEWLINE;
     74        $event .= "UID:{$id}" . self::NEWLINE;
     75
     76        $event .= "DTSTAMP:{$start_date_time}" . self::NEWLINE;
     77        $event .= "DTSTART;VALUE=DATE:{$start_date_time}" . self::NEWLINE;
     78        $event .= "DTEND;VALUE=DATE:{$end_date_time}" . self::NEWLINE;
     79        $event .= "CATEGORIES:WordPress" . self::NEWLINE;
     80        // Some calendars require the organizer's name and email address
     81        $event .= "ORGANIZER;CN=WordPress {$team} Team:mailto:mail@example.com" . self::NEWLINE;
     82        $event .= "SUMMARY:{$team}: {$title}" . self::NEWLINE;
     83        // Incrementing the sequence number updates the specified event
     84        $event .= "SEQUENCE:{$sequence}" . self::NEWLINE;
     85        $event .= 'STATUS:CONFIRMED' . self::NEWLINE;
     86        $event .= 'TRANSP:OPAQUE' . self::NEWLINE;
     87
     88        if (!empty($location)) {
     89            $event .= "LOCATION:{$location}" . self::NEWLINE;
     90        }
     91
     92        if (!empty($description)) {
     93            $event .= "DESCRIPTION:{$description}" . self::NEWLINE;
     94        }
     95
     96        if (!is_null($frequency)) {
     97            $event .= "RRULE:FREQ={$frequency}" . self::NEWLINE;
     98        }
     99
     100        $event .= "END:VEVENT" . self::NEWLINE;
     101
     102        return $event;
     103    }
     104
     105    private function get_frequency($recurrence, $date, $occurrences)
     106    {
     107        switch ($recurrence) {
     108            case 'weekly':
     109                $frequency = 'WEEKLY';
     110                break;
     111            case 'biweekly':
     112                $frequency = 'WEEKLY;INTERVAL=2';
     113                break;
     114            case 'monthly':
     115                $frequency = 'MONTHLY';
     116                break;
     117            case 'occurrence':
     118                $frequency = $this->get_frequencies_by_day($occurrences, $date);
     119                break;
     120            default:
     121                $frequency = null;
     122        }
     123
     124        return $frequency;
     125    }
     126
     127    /**
     128     * Returns a comma separated list of days in which the event should repeat for the month.
     129     *
     130     * For example, given:
     131     *   $occurrences = array( 1, 3 ) // 1st and 3rd week in the month
     132     *   $date = '2019-09-15' // the day is Sunday
     133     * it will return: 'MONTHLY;BYDAY=1SU,3SU'
     134     *
     135     * @param array $occurrences
     136     * @param string $date
     137     * @return string
     138     */
     139    private function get_frequencies_by_day($occurrences, $date)
     140    {
     141        // Get the first two letters of the day of the start date in uppercase letters
     142        $day = strtoupper(
     143            substr(strftime('%a', strtotime($date)), 0, 2)
     144        );
     145
     146        $by_days = array_reduce(array_keys($occurrences), function ($carry, $key) use ($day, $occurrences) {
     147            $carry .= $occurrences[$key] . $day;
     148
     149            if ($key < count($occurrences) - 1) {
     150                $carry .= ',';
     151            }
     152
     153            return $carry;
     154        });
     155
     156        return "MONTHLY;BYDAY={$by_days}";
     157    }
     158}
  • new file wordpress.org/public_html/wp-content/plugins/wporg-meeting-ical/wporg-meeting-ical.php

    diff --git wordpress.org/public_html/wp-content/plugins/wporg-meeting-ical/wporg-meeting-ical.php wordpress.org/public_html/wp-content/plugins/wporg-meeting-ical/wporg-meeting-ical.php
    new file mode 100644
    index 000000000..4d9b98a78
    - +  
     1<?php
     2/*
     3Plugin Name: WPORG Make Homepage Meeting iCalendar API
     4Description: Provides API endpoints for iCalendar files that are generated dynamically from WPORG Meetings
     5Version:     1.0
     6License:     GPLv2 or later
     7Author:      WordPress.org
     8Author URI:  http://wordpress.org/
     9Text Domain: wporg
     10*/
     11
     12require __DIR__ . '/class-ical-generator.php';
     13
     14class Meeting_ICAL
     15{
     16
     17    private static $instance = NULL;
     18
     19    const QUERY_KEY = 'meeting_ical';
     20    const QUERY_TEAM_KEY = 'meeting_team';
     21
     22    public static function getInstance()
     23    {
     24        NULL === self::$instance && self::$instance = new self;
     25        return self::$instance;
     26    }
     27
     28    public static function init()
     29    {
     30        $mi = Meeting_ICAL::getInstance();
     31
     32        register_activation_hook(__FILE__, array($mi, 'on_activate'));
     33        register_deactivation_hook(__FILE__, array($mi, 'on_deactivate'));
     34
     35        add_action('init', array($mi, 'add_rewrite_rules'));
     36        add_action('parse_request', array($mi, 'parse_request'));
     37
     38        add_filter('query_vars', array($mi, 'query_vars'));
     39    }
     40
     41    public function on_activate()
     42    {
     43        $this->add_rewrite_rules();
     44        flush_rewrite_rules();
     45    }
     46
     47    public function on_deactivate()
     48    {
     49        flush_rewrite_rules(); // remove custom rewrite rule
     50        delete_option(self::QUERY_KEY); // remove cache
     51    }
     52
     53    public function add_rewrite_rules()
     54    {
     55        add_rewrite_rule(
     56            '^meetings/?([a-zA-Z\d\s_-]+)?/calendar\.ics$',
     57            array(self::QUERY_KEY => 1, self::QUERY_TEAM_KEY => '$matches[1]'),
     58            'top'
     59        );
     60    }
     61
     62    public function parse_request($request)
     63    {
     64        if (!array_key_exists(self::QUERY_KEY, $request->query_vars)) {
     65            return;
     66        }
     67
     68        $team = strtolower($request->query_vars[self::QUERY_TEAM_KEY]);
     69
     70        // Generate a calendar if such a team exists
     71        if ($ical = $this->get_ical_contents($team)) {
     72            /**
     73             * If the calendar has a 'method' property, the 'Content-Type' header must also specify it
     74             */
     75            header('Content-Type: text/calendar; charset=utf-8; method=publish');
     76            header('Content-Disposition: inline; filename=calendar.ics');
     77            echo $ical;
     78            exit;
     79        }
     80
     81        return;
     82    }
     83
     84    public function query_vars($query_vars)
     85    {
     86        array_push($query_vars, self::QUERY_KEY);
     87        array_push($query_vars, self::QUERY_TEAM_KEY);
     88        return $query_vars;
     89    }
     90
     91    private function get_ical_contents($team)
     92    {
     93        $ttl = 60; // in seconds
     94        $option = $team ? self::QUERY_KEY . "_{$team}" : self::QUERY_KEY;
     95        $cache = get_option($option, false);
     96
     97        if (is_array($cache) && $cache['timestamp'] > time() - $ttl) {
     98            return $cache['contents'];
     99        }
     100
     101        if ($contents = $this->generate_ical_contents($team)) {
     102            $cache = array('contents' => $contents, 'timestamp' => time());
     103            delete_option($option);
     104            add_option($option, $cache, false, false);
     105
     106            return $cache['contents'];
     107        }
     108
     109        return null;
     110    }
     111
     112    private function generate_ical_contents($team)
     113    {
     114        $posts = $this->get_meeting_posts($team);
     115
     116        // Don't generate a calendar if there are no meetings for that team
     117        if (empty($posts)) {
     118            return null;
     119        }
     120
     121        $ical_generator = new Meeting_ICAL_Generator();
     122        return $ical_generator->generate($posts);
     123    }
     124
     125    /**
     126     * Get all meetings for a team. If the 'team' parameter is empty, all meetings are returned.
     127     *
     128     * @param string $team Name of the team to fetch meetings for.
     129     * @return array
     130     */
     131    private function get_meeting_posts($team = '')
     132    {
     133        // meta query to eliminate expired meetings from query
     134        add_filter('get_meta_sql', function ($sql) {
     135            return str_replace("'CURDATE()'", 'CURDATE()', $sql);
     136        });
     137
     138        switch_to_blog(get_main_site_id());
     139
     140        $query = new WP_Query(
     141            array(
     142                'post_type' => 'meeting',
     143                'nopaging' => true,
     144                'meta_query' => array(
     145                    'relation' => 'AND',
     146                    array(
     147                        'key' => 'team',
     148                        'value' => $team,
     149                        'compare' => empty($team) ? '!=' : '=',
     150                    ),
     151                    array(
     152                        'relation' => 'OR',
     153                        // not recurring  AND start_date >= CURDATE() = one-time meeting today or still in future
     154                        array(
     155                            'relation' => 'AND',
     156                            array(
     157                                'key' => 'recurring',
     158                                'value' => array('weekly', 'biweekly', 'occurrence', 'monthly', '1'),
     159                                'compare' => 'NOT IN',
     160                            ),
     161                            array(
     162                                'key' => 'start_date',
     163                                'type' => 'DATE',
     164                                'compare' => '>=',
     165                                'value' => 'CURDATE()',
     166                            )
     167                        ),
     168                        // recurring = 1 AND ( end_date = '' OR end_date > CURDATE() ) = recurring meeting that has no end or has not ended yet
     169                        array(
     170                            'relation' => 'AND',
     171                            array(
     172                                'key' => 'recurring',
     173                                'value' => array('weekly', 'biweekly', 'occurrence', 'monthly', '1'),
     174                                'compare' => 'IN',
     175                            ),
     176                            array(
     177                                'relation' => 'OR',
     178                                array(
     179                                    'key' => 'end_date',
     180                                    'value' => '',
     181                                    'compare' => '=',
     182                                ),
     183                                array(
     184                                    'key' => 'end_date',
     185                                    'type' => 'DATE',
     186                                    'compare' => '>',
     187                                    'value' => 'CURDATE()',
     188                                )
     189                            )
     190                        ),
     191                    )
     192                )
     193            )
     194        );
     195
     196        restore_current_blog();
     197
     198        return $query->posts;
     199    }
     200}
     201
     202Meeting_ICAL::init();
  • wordpress.org/public_html/wp-content/plugins/wporg-meeting-posttype/wporg-meeting-posttype.php

    diff --git wordpress.org/public_html/wp-content/plugins/wporg-meeting-posttype/wporg-meeting-posttype.php wordpress.org/public_html/wp-content/plugins/wporg-meeting-posttype/wporg-meeting-posttype.php
    index 9dd64b010..ff3b2ed63 100644
     
    22/*
    33Plugin Name: WPORG Make Homepage Meeting Post Type
    44Description: Creates the meeting post type and assorted filters for https://make.wordpress.org/meetings
    5 Version:     1.0
     5Version:     1.1
    66License:     GPLv2 or later
    77Author:      WordPress.org
    88Author URI:  http://wordpress.org/
    class Meeting_Post_Type { 
    253253                $occurrence = isset( $meta['occurrence'][0] ) ? unserialize( $meta['occurrence'][0] ) : array();
    254254                $link       = isset( $meta['link'][0] ) ? $meta['link'][0] : '';
    255255                $location   = isset( $meta['location'][0] ) ? $meta['location'][0] : '';
     256        $sequence   = isset( $meta['sequence'][0] ) ? $meta['sequence'][0] : 0;
    256257                wp_nonce_field( 'save_meeting_meta_'.$post->ID , 'meeting_nonce' );
    257258                ?>
    258259
    class Meeting_Post_Type { 
    326327                        <input type="text" name="location" id="location" class="regular-text wide" value="<?php echo esc_attr( $location ); ?>">
    327328                </label>
    328329                </p>
     330        <input type="hidden" name="sequence" value="<?php echo esc_attr( $sequence ); ?>">
    329331                <script>
    330332                jQuery(document).ready( function($) {
    331333                        $('.date').datepicker({
    class Meeting_Post_Type { 
    381383                                         ? array_map( 'intval', $_POST['occurrence'] ) : array() );
    382384                $meta['link']        = ( isset( $_POST['link'] ) ? esc_url( $_POST['link'] ) : '' );
    383385                $meta['location']    = ( isset( $_POST['location'] ) ? esc_textarea( $_POST['location'] ) : '' );
     386        $meta['sequence']    = ( isset( $_POST['sequence'] ) ? intval( $_POST['sequence'] ) + 1 : 0 );
    384387
    385388                foreach ( $meta as $key => $value ) {
    386389                        update_post_meta( $post->ID, $key, $value );