Add a new landing page design and site-wide design detail updates for webkit.org
[WebKit-https.git] / Websites / webkit.org / wp-content / themes / webkit / functions.php
1 <?php
2
3 // Declare theme features
4 add_theme_support( 'post-thumbnails' );
5
6 add_action( 'init', function () {
7     register_nav_menu('site-nav', __( 'Site Navigation' ));
8     register_nav_menu('footer-nav', __( 'Footer Navigation' ));
9     register_nav_menu('sitemap', __( 'Site Map Page' ));
10 } );
11
12 add_action( 'wp_dashboard_setup', function () {
13     $SurveyWidget = new WebKit_Nightly_Survey();
14     $SurveyWidget->add_widget();
15 });
16
17 function modify_contact_methods($profile_fields) {
18
19     // Add new fields
20     $profile_fields['twitter'] = 'Twitter Handle';
21     unset($profile_fields['aim']);
22     unset($profile_fields['yim']);
23     unset($profile_fields['jabber']);
24
25     return $profile_fields;
26 }
27
28 function get_nightly_build ($type = 'builds') {
29     if (!class_exists('SyncWebKitNightlyBuilds'))
30         return false;
31
32     $WebKitBuilds = SyncWebKitNightlyBuilds::object();
33     $build = $WebKitBuilds->latest($type);
34     return $build;
35 }
36
37 function get_nightly_source () {
38     return get_nightly_build('source');
39 }
40
41 function get_nightly_archives ($limit) {
42     if (!class_exists('SyncWebKitNightlyBuilds'))
43         return array();
44
45     $WebKitBuilds = SyncWebKitNightlyBuilds::object();
46     $builds = $WebKitBuilds->records('builds', $limit);
47     return (array)$builds;
48 }
49
50 function get_nightly_builds_json () {
51     if (!class_exists('SyncWebKitNightlyBuilds'))
52         return '';
53
54     $WebKitBuilds = SyncWebKitNightlyBuilds::object();
55     $records = $WebKitBuilds->records('builds', 100000);
56     $builds = array();
57     foreach ( $records as $build ) {
58         $builds[] = $build[0];
59     }
60     $json = json_encode($builds);
61     return empty($json) ? "''" : $json;
62 }
63
64 add_filter('user_contactmethods', function ($fields) {
65     // Add Twitter field to user profiles
66     $fields['twitter'] = 'Twitter Handle';
67
68     // Remove unused social networks
69     unset($fields['aim']);
70     unset($fields['yim']);
71     unset($fields['jabber']);
72
73     return $fields;
74 });
75
76 add_action('init', function () {
77     register_sidebar(array(
78         'name'=> 'Home Tiles',
79         'id' => 'tiles',
80         'before_widget' => '',
81         'after_widget' => '',
82         'before_title' => '',
83         'after_title' => '',
84     ));
85 } );
86
87 // Start Page internal rewrite handling
88 add_action('after_setup_theme', function () {
89     add_rewrite_rule(
90         'nightly/start/([^/]+)/([0-9]+)/?$',
91         'index.php?pagename=nightly/start&nightly_branch=$matches[1]&nightly_build=$matches[2]',
92         'top'
93     );
94 });
95
96 add_filter('query_vars', function( $query_vars ) {
97     $query_vars[] = 'nightly_build';
98     $query_vars[] = 'nightly_branch';
99     return $query_vars;
100 });
101
102 add_filter('the_title', function( $title ) {
103     if ( is_admin() ) return $title;
104     if ( is_feed() ) return $title;
105     
106     $title = str_replace(": ", ": <br>", $title);
107
108     $nowrap_strings = array();
109     if ($nowrap_setting = get_option("webkit_org_nowrap_strings")) {
110         $nowrap_strings = explode("\n", $nowrap_setting);
111     } else add_option("webkit_org_nowrap_strings", "\n");
112
113     foreach ($nowrap_strings as $token) {
114         $nobreak = str_replace(" ", " ", trim($token));
115         $title = str_replace(trim($token), $nobreak, $title);
116     }
117     
118     return $title;
119 });
120
121 // For RSS feeds, convert relative URIs to absolute
122 add_filter('the_content', function($content) {
123     if (!is_feed()) return $content;
124     $base = trailingslashit(get_site_url());
125     $content = preg_replace('/<a([^>]*) href="\/([^"]*)"/', '<a$1 href="' . $base . '$2"', $content);
126     $content = preg_replace('/<img([^>]*) src="\/([^"]*)"/', '<img$1 src="' . $base . '$2"', $content);
127     return $content;
128 });
129
130 add_action('wp_head', function () {
131     if (!is_single()) return;
132
133     $style = get_post_meta(get_the_ID(), 'style', true);
134     if (!empty($style))
135         echo '<style type="text/css">' . $style . '</style>';
136
137     $script = get_post_meta(get_the_ID(), 'script', true);
138     if (!empty($script))
139         echo '<script type="text/javascript">' . $script . '</script>';
140 });
141
142 add_action('the_post', function($post) {
143     global $pages;
144
145     if (!(is_single() || is_page())) return;
146
147     $content = $post->post_content;
148     if (strpos($content, 'abovetitle') === false) return;
149     if (strpos($content, '<img') !== 0) return;
150
151     $post->post_title_img = substr($content, 0, strpos($content, ">\n") + 3);
152     $post->post_content = str_replace($post->post_title_img, '', $content);
153     $pages = array($post->post_content);
154 });
155
156 add_action('the_post', function($post) {
157     global $pages;
158
159     if (!(is_single() || is_page())) return;
160
161     $foreword = get_post_meta(get_the_ID(), 'foreword', true);
162     if ( ! $foreword ) return;
163
164     $content = $post->post_content;
165     // Transform Markdown
166     $Markdown = WPCom_Markdown::get_instance();
167     $foreword = wp_unslash( $Markdown->transform($foreword) );
168
169     $post->post_content = '<div class="foreword">' . $foreword . '</div>' . $content;
170     $pages = array($post->post_content);
171 });
172
173 function before_the_title() {
174     $post = get_post();
175
176     if ( isset($post->post_title_img) )
177         echo wp_make_content_images_responsive($post->post_title_img);
178 }
179
180 // Hide category 41: Legacy from archives
181 add_filter('pre_get_posts', function ($query) {
182     if ( $query->is_home() )
183         $query->set('cat', '-41');
184     return $query;
185 });
186
187 add_filter( 'get_the_excerpt', function( $excerpt ) {
188     $sentences = preg_split( '/(\.|!|\?)\s/', $excerpt, 2, PREG_SPLIT_DELIM_CAPTURE );
189
190     // if ( empty($sentences[1]) )
191     //     $sentences[1] = '&hellip;';
192
193     return $sentences[0] . $sentences[1];
194
195 });
196
197 function show_404_page() {
198     status_header(404);
199     return include( get_query_template( '404' ) );
200 }
201
202 include('widgets/post.php');
203 include('widgets/icon.php');
204 include('widgets/twitter.php');
205 include('widgets/page.php');
206
207 function table_of_contents() {
208     if ( class_exists('WebKitTableOfContents') )
209         WebKitTableOfContents::markup();
210 }
211
212 function has_table_of_contents() {
213     if ( class_exists('WebKitTableOfContents') )
214         return WebKitTableOfContents::hasIndex();
215 }
216
217 function table_of_contents_index( $content, $post_id ) {
218     if ( ! class_exists('WebKitTableOfContents') )
219         return $content;
220     $content = WebKitTableOfContents::parse($content);
221     WebKitTableOfContents::wp_insert_post($post_id);
222     return $content;
223 }
224
225 function is_super_cache_enabled() {
226     global $super_cache_enabled;
227     return (isset($super_cache_enabled) && true === $super_cache_enabled);
228 }
229
230 function tag_post_image_luminance( $post_id ) {
231     $threshold = 128;
232     $tags = array();
233
234     // Get the image data
235     $image_src = wp_get_attachment_image_src( get_post_thumbnail_id($post_id), 'small' );
236     $image_url = $image_src[0];
237
238     if ( empty($image_url) ) return $post_id;
239
240     // detect luminence value
241     $luminance = calculate_image_luminance($image_url);
242     $tags = wp_get_post_tags($post_id, array('fields' => 'names'));
243
244     if ( $luminance < $threshold )
245         $tags[] = 'dark';
246     elseif ( false !== ( $key = array_search('dark', $messages) ) )
247         unset($tags[ $key ]);
248
249     // Set a tag class
250     if ( ! empty($tags) )
251         wp_set_post_tags( $post_id, $tags, true );
252
253     return $post_id;
254 }
255
256 function calculate_image_luminance($image_url) {
257     if (!function_exists('ImageCreateFromString'))
258         return 1;
259
260     // Get original image dimensions
261     $size = getimagesize($image_url);
262
263     // Create image resource from source image
264     $image = ImageCreateFromString(file_get_contents($image_url));
265
266     // Allocate image resource
267     $sample = ImageCreateTrueColor(1, 1);
268
269     // Flood fill with a white background (to properly calculate luminance of PNGs with alpha)
270     ImageFill($sample , 0, 0, ImageColorAllocate($sample, 255, 255, 255));
271
272     // Downsample to 1x1 image
273     ImageCopyResampled($sample, $image, 0, 0, 0, 0, 1, 1, $size[0], $size[1]);
274
275     // Get the RGB value of the pixel
276     $rgb   = ImageColorAt($sample, 0, 0);
277     $red   = ($rgb >> 16) & 0xFF;
278     $green = ($rgb >> 8) & 0xFF;
279     $blue  = $rgb & 0xFF;
280
281     // Calculate relative luminance value (https://en.wikipedia.org/wiki/Relative_luminance)
282     return ( 0.2126 * $red + 0.7152 * $green + 0.0722 * $blue);
283 }
284
285 function html_select_options(array $list, $selected = null, $values = false, $append = false) {
286         if ( ! is_array($list) ) return '';
287
288         $_ = '';
289
290         // Append the options if the selected value doesn't exist
291         if ( ( ! in_array($selected, $list) && ! isset($list[ $selected ])) && $append )
292             $_ .= '<option value="' . esc_attr($selected) . '">' .esc_html($selected) . '</option>';
293
294         foreach ( $list as $value => $text ) {
295
296             $value_attr = $selected_attr = '';
297
298             if ( $values ) $value_attr = ' value="' . esc_attr($value) . '"';
299             if ( ( $values && (string)$value === (string)$selected)
300                 || ( ! $values && (string)$text === (string)$selected ) )
301                     $selected_attr = ' selected="selected"';
302
303             if ( is_array($text) ) {
304                 $label = $value;
305                 $_ .= '<optgroup label="' . esc_attr($label) . '">';
306                 $_ .= html_select_options($text, $selected, $values);
307                 $_ .= '</optgroup>';
308                 continue;
309             } else $_ .= "<option$value_attr$selected_attr>$text</option>";
310
311         }
312         return $_;
313     }
314
315 add_filter('save_post', 'tag_post_image_luminance');
316
317 add_filter('next_post_link', function ( $format ) {
318     return str_replace('href=', 'class="page-numbers next-post" href=', $format);
319 });
320 add_filter('previous_post_link', function ( $format ) {
321     return str_replace('href=', 'class="page-numbers prev-post" href=', $format);
322 });
323
324 // Queue global scripts
325 add_action( 'wp_enqueue_scripts', function () {
326     wp_register_script(
327         'theme-global',
328         get_stylesheet_directory_uri() . '/scripts/global.js',
329         false,
330         '1.0',
331         true
332     );
333
334     wp_enqueue_script( 'theme-global' );
335
336 } );
337
338 class Responsive_Toggle_Walker_Nav_Menu extends Walker_Nav_Menu {
339
340     private $toggleid = null;
341
342     public function start_lvl( &$output, $depth = 0, $args = array() ) {
343         $output .= "\n" . str_repeat("\t", $depth);
344         $classes = array("sub-menu");
345         if ( 0 == $depth ) {
346             $classes[] = "sub-menu-layer";
347         }
348         $id = ( 0 == $depth ) ? " id=\"sub-menu-for-{$this->toggleid}\"" : '';
349         $class_names = esc_attr(join( ' ', $classes ));
350         $output .= "<ul class=\"$class_names\" role=\"menu\"$id>\n";
351     }
352
353     public function end_lvl( &$output, $depth = 0, $args = array() ) {
354         $indent = str_repeat("\t", $depth);
355         $output .= "$indent</ul>\n";
356     }
357
358     public function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) {
359
360         $before = $args->link_before;
361         $after = $args->link_after;
362
363         if ( in_array('menu-item-has-children', $item->classes) && 0 == $depth ) {
364             $this->toggleid = $item->ID;
365             $args->before .= "<input type=\"checkbox\" id=\"toggle-{$item->ID}\" class=\"menu-toggle\" />";
366             $args->link_before = "<label for=\"toggle-{$item->ID}\" class=\"label-toggle\">" . $args->link_before;
367             $args->link_after .= "</label>";
368             $item->url = '#nav-sub-menu';
369         } elseif ( in_array('menu-item-has-children', $item->classes) && 1 == $depth ) {
370             // $item->role = "presentation";
371         } else $toggleid = null;
372
373         $indent = ( $depth ) ? str_repeat( "\t", $depth ) : '';
374
375         $classes = empty( $item->classes ) ? array() : (array) $item->classes;
376         $classes[] = 'menu-item-' . $item->ID;
377
378         $class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args, $depth ) );
379         $class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';
380
381         $id = apply_filters( 'nav_menu_item_id', 'menu-item-'. $item->ID, $item, $args, $depth );
382         $id = $id ? ' id="' . esc_attr( $id ) . '"' : '';
383
384         $output .= $indent . '<li' . $id . $class_names . '>';
385
386         $atts = array();
387         $atts['title']  = ! empty( $item->attr_title ) ? $item->attr_title : '';
388         $atts['target'] = ! empty( $item->target )     ? $item->target     : '';
389         $atts['rel']    = ! empty( $item->xfn )        ? $item->xfn        : '';
390         $atts['href']   = ! empty( $item->url )        ? $item->url        : '';
391         $atts['role']   = ! empty( $item->role )       ? $item->role       : '';
392
393         if ( in_array('menu-item-has-children', $item->classes) && 0 == $depth ) {
394             $atts['aria-haspopup'] = "true";
395             $atts['aria-owns'] = 'sub-menu-for-' . $item->ID;
396             $atts['aria-controls'] = 'sub-menu-for-' . $item->ID;
397             $atts['aria-expanded'] = 'true';
398         }
399
400         $atts = apply_filters( 'nav_menu_link_attributes', $atts, $item, $args, $depth );
401
402         $attributes = '';
403         foreach ( $atts as $attr => $value ) {
404             if ( ! empty( $value ) ) {
405                 $value = ( 'href' === $attr ) ? esc_url( $value ) : esc_attr( $value );
406                 $attributes .= ' ' . $attr . '="' . $value . '"';
407             }
408         }
409
410         $item_output = $args->before;
411         $item_output .= '<a'. $attributes .'>';
412         $item_output .= $args->link_before . apply_filters( 'the_title', $item->title, $item->ID ) . $args->link_after;
413         $item_output .= '</a>';
414         $item_output .= $args->after;
415
416         $output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
417
418         $args->link_before = $before;
419         $args->link_after = $after;
420     }
421
422 }
423
424 class Front_Page_Posts {
425
426     private static $object;     // Singleton instance
427
428     private static $wp_query;   // WP_Query instance
429
430     public static function &object () {
431         if ( ! self::$object instanceof self )
432             self::$object = new self;
433
434         if ( empty(self::$wp_query) )
435             self::$wp_query = new WP_Query(array('post_type' => 'post', 'posts_per_page' => 16));
436
437         return self::$object;
438     }
439
440     public static function WP_Query() {
441         return self::$wp_query;
442     }
443
444 }
445
446 class WebKit_Nightly_Survey {
447
448     const COOKIE_PREFIX = 'webkitnightlysurvey_';
449     const DATA_SETTING_NAME = 'webkit_nightly_survey_data';
450     const SURVEY_FILENAME = 'survey.json';
451
452     public function add_widget() {
453
454         wp_add_dashboard_widget(
455             'webkit_nightly_survey_results', // Widget slug
456             'WebKit Nightly Survey Results', // Title
457             array($this, 'display_widget')   // Display function
458         );
459
460         if (isset($_GET['wksurvey']) && $_GET['wksurvey'] == 'download')
461             $this->download() && exit;
462
463     }
464
465     public function display_widget() {
466         $data = get_option(self::DATA_SETTING_NAME);
467         $styles = "
468             <style>
469             .survey_table .question {
470                 text-align: left;
471             }
472             .survey_table .score {
473                 font-size: 15px;
474                 min-width: 30px;
475                 text-align: right;
476                 vertical-align: top;
477                 padding-right: 10px;
478             }
479             .survey_table .others {
480                 font-style: italic;
481             }
482             </style>
483         ";
484
485         $response_limit = 10;
486         $table = '';
487         foreach ($data as $question => $responses) {
488             $question_row = '<tr><th colspan="2" class="question">' . $question . '</th></tr>';
489             $response_rows = '';
490             arsort($responses);
491             $response_count = 0;
492             $total_responses = count($responses);
493             foreach ($responses as $response => $votes) {
494                 $response_rows .= '<tr><td class="score">' . intval($votes) . '</td><td class="response">' . stripslashes($response) . '</td></tr>';
495                 if ( ++$response_count >= $response_limit && $response_count < $total_responses) {
496                     $response_rows .= '<tr><td class="score others">' . intval($total_responses - $response_limit) . '</td><td class="response others">more "other" responses</td></tr>';
497                     break;
498                 }
499             }
500             $table .= $question_row . $response_rows;
501         }
502         echo $styles;
503         echo '<table class="survey_table">' . $table . '</table>';
504         echo '<p class="textright"><a class="button button-primary" href="' . add_query_arg('wksurvey','download',admin_url()) . '">Download Results as CSV</a></p>';
505     }
506
507     private function download() {
508         $data = get_option(self::DATA_SETTING_NAME);
509         $table = '';
510
511         header('Content-type: text/csv; charset=UTF-8');
512         header('Content-Disposition: attachment; filename="webkit_nightly_survey.csv"');
513         header('Content-Description: Delivered by webkit.org');
514         header('Cache-Control: maxage=1');
515         header('Pragma: public');
516
517         foreach ($data as $question => $responses) {
518             $question_row = 'Score,' . $question . "\n";
519             $response_rows = '';
520             arsort($responses);
521             foreach ($responses as $response => $votes) {
522                 $response_rows .= intval($votes) . ',' . stripslashes($response) . "\n";
523             }
524             $table .= $question_row . $response_rows . "\n";
525         }
526         echo $table;
527
528         exit;
529     }
530
531     public function process() {
532         if ( empty($_POST) ) return;
533
534         if ( ! wp_verify_nonce($_POST['_nonce'], self::SURVEY_FILENAME) )
535             wp_die('Invalid WebKit Nightly Survey submission.');
536
537         $score = $data = get_option(self::DATA_SETTING_NAME);
538         $Survey = self::survey();
539         foreach ($Survey as $id => $SurveyQuestion) {
540             if ( ! isset($score[ $SurveyQuestion->question ]) )
541                 $score[ $SurveyQuestion->question ] = array();
542             $response = $_POST['questions'][ $id ];
543             $answer = empty($SurveyQuestion->responses[ $response ]) ? $response : $SurveyQuestion->responses[ $response ];
544             if ($answer == 'Other:')
545                 $answer = $_POST['other'][ $id ];
546             $score[ $SurveyQuestion->question ][ $answer ]++;
547         }
548
549         if ( $data === false ) {
550             $deprecated = null;
551             $autoload = 'no';
552             add_option(self::DATA_SETTING_NAME, $score, $deprecated, $autoload);
553         } else {
554             update_option(self::DATA_SETTING_NAME, $score);
555         }
556         
557         $httponly = false;
558         $secure = false;
559         setcookie(self::cookie_name(), 1, time() + YEAR_IN_SECONDS, '/', WP_HOST, $secure, $httponly );
560         $_COOKIE[ self::cookie_name() ] = 1;
561     }
562
563     public static function responded() {
564         return isset($_COOKIE[ self::cookie_name() ]);
565     }
566
567     private static function cookie_name() {
568         return self::COOKIE_PREFIX . COOKIEHASH;
569     }
570
571     public static function survey_file() {
572         return dirname(__FILE__) . '/' . self::SURVEY_FILENAME;
573     }
574
575     public static function form_nonce() {
576         return '<input type="hidden" name="_nonce" value="' . wp_create_nonce( self::SURVEY_FILENAME ) . '">';
577     }
578
579     public static function survey () {
580         $survey_json = file_get_contents(self::survey_file());
581         $Decoded = json_decode($survey_json);
582         return isset($Decoded->survey) ? $Decoded->survey : array();
583     }
584
585 }