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