├── .gitattributes ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── LICENSE ├── README.md ├── azure-pipelines.yml ├── composer.json ├── data ├── GeoLite2-City.mmdb └── qqwry.dat ├── includes └── index.php ├── package.json ├── scripts ├── bundle.sh └── push.sample.sh ├── src ├── images │ └── wave.svg ├── package.scss ├── php │ ├── 404.php │ ├── README.md │ ├── archive.php │ ├── author.php │ ├── category.php │ ├── classes │ │ ├── comments-post-handle.php │ │ ├── get-comment-child-list.php │ │ ├── get-comment-list.php │ │ ├── get-ip-location.php │ │ ├── get-user-agent.php │ │ ├── graphql-register-fields.php │ │ ├── graphql-register-mutate.php │ │ ├── graphql-register-types.php │ │ ├── lib │ │ │ ├── data-base-update.php │ │ │ └── ip-location-qqwary.php │ │ ├── sakura-error.php │ │ ├── sakura-register-actions.php │ │ ├── sakura-walker-drawer-nav.php │ │ └── sakura-walker-header-nav.php │ ├── comments.php │ ├── footer.php │ ├── functions.php │ ├── header.php │ ├── index.php │ ├── layout │ │ ├── comment-form.php │ │ ├── comment-list.php │ │ ├── drawer.php │ │ ├── footer-bar.php │ │ ├── head.php │ │ ├── header-top-after.php │ │ ├── header-top.php │ │ ├── pagination.php │ │ ├── post-feature-fit.php │ │ ├── post-feature-full.php │ │ ├── post-list-loop.php │ │ ├── searchform.php │ │ ├── sidebar-left.php │ │ ├── sidebar-right.php │ │ ├── site-header.php │ │ ├── snackbar.php │ │ └── top-app-bar.php │ ├── page.php │ ├── search.php │ ├── single.php │ ├── tag.php │ ├── templates │ │ ├── README.md │ │ ├── content-empty.php │ │ ├── content-normal.php │ │ ├── post-list-item-sakura.php │ │ ├── post-list-item-sakura_reverse.php │ │ ├── post-list-noting.php │ │ └── test.php │ └── utils │ │ ├── disable-wp-emoji.php │ │ ├── redux-config.php │ │ ├── redux-demo-config.php │ │ ├── sakura-admin.php │ │ ├── sakura-debug.php │ │ ├── sakura-functions.php │ │ └── sakura-tools.php ├── screenshot.png ├── scss │ ├── components │ │ ├── mdc.scss │ │ └── pjax.scss │ ├── index.scss │ ├── layouts │ │ ├── comment.scss │ │ ├── coverImage.scss │ │ ├── drawer.scss │ │ ├── footer.scss │ │ ├── header.scss │ │ ├── ini.scss │ │ ├── pageContent.scss │ │ ├── pageNavigationBar.scss │ │ ├── postFeature.scss │ │ ├── postThumbList.scss │ │ └── top-app-bar.scss │ ├── markdown │ │ ├── github.scss │ │ └── markdown.scss │ └── variables.scss └── ts │ ├── classes │ ├── createComment.ts │ ├── drawer.ts │ ├── graphqQuerry.ts │ ├── graphqlMutate.ts │ ├── pageNavigationBar.ts │ └── showCommentList.ts │ ├── functions │ ├── components │ │ ├── mdcConf.ts │ │ ├── mdcInit.ts │ │ └── pjaxInit.ts │ ├── event │ │ ├── README.md │ │ ├── backReload.ts │ │ ├── onloadInit.ts │ │ └── pjaxReload.ts │ ├── module │ │ ├── commentInit.ts │ │ ├── coverParallax.ts │ │ ├── footerWave.ts │ │ ├── headerBarScrollHandler.ts │ │ └── snackbar.ts │ ├── page │ │ ├── init.ts │ │ └── postInit.ts │ └── util │ │ ├── hexFilter.ts │ │ ├── rgb2hex.ts │ │ └── scrollDirection.ts │ ├── graphql │ ├── mutateCreateComment.gql │ ├── queryCommentChildListById.gql │ └── queryCommentListById.gql │ └── index.ts ├── tsconfig.json └── webpack.config.js /.gitattributes: -------------------------------------------------------------------------------- 1 | includes/* linguist-vendored 2 | dist/* linguist-generated 3 | docs/* linguist-documentation 4 | data/* linguist-detectable=false 5 | includes/* linguist-detectable=false 6 | dist/* linguist-detectable=false -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [8.x, 10.x, 12.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: npm install, build, and test 21 | run: | 22 | npm install -g webpack webpack-cli --save-dev 23 | npm install 24 | npx webpack --config webpack.config.js 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | includes/vendor 4 | push.bat 5 | pull.bat 6 | push.sh 7 | pull.sh 8 | package-lock.json 9 | composer.lock 10 | # *.dat 11 | # *.mmndb -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Mashiro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Sakura v2 2 | ==== 3 | 4 | [![Demo](https://shader.2heng.xin/badge/Demo-dev.2heng.xin-critical?logo=envato)](https://dev.2heng.xin) [![Build Status](https://dev.azure.com/moezhx/sakura2/_apis/build/status/mashirozx.sakura2?branchName=master)](https://dev.azure.com/moezhx/sakura2/_build/latest?definitionId=1&branchName=master) [![webpack](https://shader.2heng.xin/badge/webpack-4.41.2-9cf?logo=webpack)](https://webpack.js.org/) [![wordpress](https://shader.2heng.xin/badge/WordPress-5.3-blue?logo=wordpress)](https://wordpress.org) [![typescript](https://shader.2heng.xin/badge/TypeScript-3.6.4-294E80?logo=typescript)](https://www.typescriptlang.org/) [![php](https://shader.2heng.xin/badge/PHP-7.2-8892BF?logo=php)](https://www.php.net/) 5 | 6 | __This repo is under developement, do not active in production environment!__ 7 | 8 | ### Install 9 | Clone/download into `./wp-content/themes/`, install the depended plugins [WPGraphQL](https://github.com/wp-graphql/wp-graphql) (for GraphQL API support) and [Redux Framework](https://cn.wordpress.org/plugins/redux-framework/) (for options framework support), then active the theme in WordPress dashboard after compiling. 10 | 11 | ### Compile 12 | ```bash 13 | # Install dependencies 14 | npm i -g webpack-cli 15 | npm install 16 | curl -sS https://getcomposer.org/installer | php 17 | php composer.phar install 18 | # Dynamical compile (--watch file changes) 19 | npm run dev 20 | # Build 21 | npm run build 22 | ``` 23 | 24 | __Commands tested on Linux__, should also work on other platforms. 25 | 26 | ### Develop 27 | __DO NOT MODIFY OR SAVE IN `./dist/`__, all your changes inside this folder will be replaced after `npm run build`. Develop by changing files in `./src/`. 28 | 29 | ``` 30 | . 31 | ├── dist // Compiled files (*DO NOT SAVE HERE*) 32 | │ ├── css // Stylesheets (compiled) 33 | │ ├── js // JavaScript (compiled) 34 | │ ├── images // Images (compiled) 35 | │ ├── *.php // PHP files (compiled) 36 | │ ├── manifest.json // Webpack manifest resources list 37 | │ └── style.css // Wordpress theme info 38 | ├── src // Source code (develop inside this folder) 39 | │ ├── scss // CSS source code (SASS) 40 | │ ├── ts // JavaScript source code (TypeScript) 41 | │ ├── images // Images resources 42 | │ ├── php // PHP source code 43 | │ └── package.scss // Wordpress theme info (header of style.css) 44 | ├── package.json // Node.js configuration 45 | ├── tsconfig.json // TypeScript configuration 46 | └── webpack.config.js // Webpack configuration 47 | ``` 48 | 49 | ### Contribute 50 | Pull requests welcomed. Code comments and commit tags in English, please. 51 | 52 | Git commit guidelines: [the Angular guidelines](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines) 53 | 54 | Code comment guidelines: [the TypeScript guidelines](https://github.com/unional/typescript-guidelines/blob/master/pages/default/draft/comments.md) 55 | 56 | ### Author 57 | Mashiro 58 | 59 | ### License 60 | MIT 61 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Node.js with webpack 2 | # Build a Node.js project using the webpack CLI. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript 5 | 6 | trigger: 7 | - master 8 | 9 | pool: 10 | vmImage: 'ubuntu-latest' 11 | 12 | steps: 13 | - task: NodeTool@0 14 | inputs: 15 | versionSpec: '10.x' 16 | displayName: 'Install Node.js' 17 | 18 | - script: | 19 | npm install -g webpack webpack-cli --save-dev 20 | npm install 21 | npx webpack --config webpack.config.js 22 | displayName: 'npm install, run webpack' 23 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mashirozx/sakura2", 3 | "description": "WordPress theme Sakura", 4 | "type": "wordpress-theme", 5 | "license": "MIT", 6 | "authors": [{ 7 | "name": "Mashiro", 8 | "email": "adadam@qq.com" 9 | }], 10 | "require": { 11 | "geoip2/geoip2": "^2.9", 12 | "paquettg/php-html-parser": "^2.1" 13 | }, 14 | "config": { 15 | "vendor-dir": "includes/vendor" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /data/GeoLite2-City.mmdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mashirozx/sakura2/1243d4014fc8fb53e16cb2bc6f31ef38e903a518/data/GeoLite2-City.mmdb -------------------------------------------------------------------------------- /data/qqwry.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mashirozx/sakura2/1243d4014fc8fb53e16cb2bc6f31ef38e903a518/data/qqwry.dat -------------------------------------------------------------------------------- /includes/index.php: -------------------------------------------------------------------------------- 1 | wave -------------------------------------------------------------------------------- /src/package.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Theme Name: Sakura (in beta) 3 | * Theme URI: https://2heng.xin 4 | * Description: A Wonderful WordPress Theme Sakura 🌸 5 | * Version: 4.0.0 6 | * Author: Mashiro 7 | * Author URI: https://2heng.xin 8 | * Tags: HTML5, CSS3, Webpack, framework 9 | * 10 | * License: MIT 11 | * License URI: http://opensource.org/licenses/mit-license.php 12 | */ -------------------------------------------------------------------------------- /src/php/404.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | 7 | 8 |
9 | 10 |

11 |

12 | 13 |

14 | 15 |
16 | 17 | 18 |
19 | 20 |
21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/php/README.md: -------------------------------------------------------------------------------- 1 | PHP templates 2 | === 3 | 4 | ``` 5 | . 6 | ├── layout // *layout particals 7 | ├── modules // *functional modules 8 | ├── templates // *page/post templates 9 | ├── 404.php // * 10 | ├── archive.php // * 11 | ├── author.php // * 12 | ├── category.php // * 13 | ├── comments.php // * 14 | ├── footer.php // * 15 | ├── functions.php // * 16 | ├── header.php // * 17 | ├── index.php // * 18 | ├── page.php // * 19 | ├── searchform.php // remove 20 | ├── search.php // * 21 | ├── sidebar.php // remove 22 | ├── single.php // * 23 | └── tag.php // * 24 | ``` -------------------------------------------------------------------------------- /src/php/archive.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | 7 |

8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/php/author.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | 7 | 8 | 9 |

10 | 11 | 12 | 13 | 14 | 15 |

16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
> 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |

36 | 37 |

38 | 39 | 40 | 41 | 42 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
53 | 54 | 55 | 56 |
57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
65 | 66 |

67 | 68 |
69 | 70 | 71 | 72 | 73 | 74 | 75 |
76 | 77 |
78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/php/category.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | 7 |

8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/php/classes/comments-post-handle.php: -------------------------------------------------------------------------------- 1 | get_error_data()); 43 | if (!empty($data)) { 44 | // Error::log('Submission Failure: ' . $comment->get_error_message()); 45 | // throw new Exception(__('Submission Failure: ', 'sakura') . $comment->get_error_message()); 46 | return array( 47 | 'succeed' => false, 48 | 'message' => $comment->get_error_message(), 49 | 'comment' => $comment, 50 | ); 51 | } else { 52 | // Error::log('Submission Failure (TYPE: ELSE)'); 53 | // throw new Exception(__('Submission Failure.', 'sakura')); 54 | return array( 55 | 'succeed' => false, 56 | 'message' => __('Submission failure (unknown reason).', 'sakura'), 57 | 'comment' => $comment, 58 | ); 59 | } 60 | } 61 | 62 | return array( 63 | 'succeed' => true, 64 | 'message' => $comment->comment_approved==1?__('Comment is submitted successfully.', 'sakura'):__('Comment is submitted successfully, but awaiting moderation.', 'sakura'), 65 | 'comment' => $comment, 66 | ); 67 | } 68 | 69 | public static function set_cookie($comment) 70 | { 71 | $user = wp_get_current_user($comment); 72 | $cookies_consent = (isset($_POST['wp-comment-cookies-consent'])); 73 | 74 | /** 75 | * Perform other actions when comment cookies are set. 76 | * 77 | * @since 3.4.0 78 | * @since 4.9.6 The `$cookies_consent` parameter was added. 79 | * 80 | * @param WP_Comment $comment Comment object. 81 | * @param WP_User $user Comment author's user object. The user may not exist. 82 | * @param boolean $cookies_consent Comment author's consent to store cookies. 83 | */ 84 | do_action('set_comment_cookies', $comment, $user, $cookies_consent); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/php/classes/get-comment-child-list.php: -------------------------------------------------------------------------------- 1 | comment_id = $commentId; 35 | $this->page_size = $pageSize; 36 | $this->target_page = $targetPage; 37 | $this->show_preview = false; 38 | $this->child_id_list = $this->get_child_id_list(); 39 | $this->comments_count = $this->get_child_comments_count(); 40 | $this->calculate(); 41 | $this->comments_array = $this->get_child_comments(); 42 | } 43 | 44 | /** 45 | * Get id of all `children` and `grandchildren` and `grandgrandchildren` 46 | * and so on of a comment 47 | * @since 4.0 48 | * 49 | * @return array 50 | */ 51 | private function get_child_id_list() 52 | { 53 | $child_id_list = array(); 54 | $child_id_list = $this->push_child_id($child_id_list, $this->comment_id); 55 | 56 | return array_map('intval', $child_id_list); 57 | } 58 | 59 | /** 60 | * SQL query for the child id 61 | * @since 4.0 62 | * 63 | * @return array 64 | */ 65 | private function push_child_id($child_id_list, $comment_id) 66 | { 67 | 68 | global $wpdb; 69 | 70 | $query = $wpdb->prepare( 71 | "SELECT comment_ID FROM {$wpdb->comments} 72 | WHERE comment_parent = %d 73 | AND comment_approved = %d 74 | ", $comment_id, 1 75 | ); 76 | 77 | $child_array = $wpdb->get_col($query); 78 | 79 | if (!empty($child_array)) { 80 | 81 | $child_id_list = array_merge($child_id_list, $child_array); 82 | 83 | foreach ($child_array as $child_comment_id) { 84 | $this->push_child_id($child_id_list, $child_comment_id); 85 | } 86 | 87 | } 88 | 89 | return $child_id_list; 90 | } 91 | 92 | /** 93 | * counter for the child_id_list array 94 | * @since 4.0 95 | * 96 | * @return int 97 | */ 98 | private function get_child_comments_count() 99 | { 100 | $count = count($this->child_id_list); 101 | if ($count == 0) { 102 | $this->has_comment = false; 103 | } else { 104 | $this->has_comment = true; 105 | } 106 | return $count; 107 | } 108 | 109 | /** 110 | * Get comment by WP_Comment_Query 111 | * @since 4.0 112 | * 113 | * @return array 114 | */ 115 | private function get_child_comments() 116 | { 117 | $args = array( 118 | 'comment__in' => $this->child_id_list, 119 | 'orderby' => 'comment_ID', 120 | 'order' => 'ASC', 121 | 'number' => $this->number, 122 | 'offset' => $this->offset, 123 | ); 124 | // TODO: we can also make a top rated comments list by order by a 'meta_key' 125 | 126 | $comments_query = new WP_Comment_Query; 127 | $comments = $comments_query->query($args); 128 | return $comments; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/php/classes/get-comment-list.php: -------------------------------------------------------------------------------- 1 | post_id = $postId; 90 | $this->page_size = $pageSize; 91 | $this->target_page = $targetPage; 92 | $this->show_preview = true; 93 | $this->comments_count = $this->get_post_comments_count(); 94 | $this->calculate(); 95 | $this->comments_array = $this->get_post_comments(); 96 | } 97 | 98 | /** 99 | * get the formed comment detail array 100 | * 101 | * @return array 102 | */ 103 | public function get_comments_formed_array() 104 | { 105 | if (!$this->has_comment) { 106 | return array(); 107 | } 108 | $comments_formed_array = []; 109 | $comments_array = $this->comments_array; 110 | foreach ($comments_array as $comment_array) { 111 | $role = $comment_array->user_id ? get_userdata($comment_array->user_id)->roles : 'visitor'; 112 | $like = get_comment_meta($comment_array->comment_ID, 'like', false); 113 | $dislike = get_comment_meta($comment_array->comment_ID, 'dislike', false); 114 | $comment_formed_array = array( 115 | 'comment_ID' => intval($comment_array->comment_ID), 116 | 'comment_parent' => intval($comment_array->comment_parent), 117 | 'comment_author' => $comment_array->comment_author, 118 | 'comment_author_avatar' => get_avatar_url($comment_array->comment_author_email), 119 | 'url' => !empty($comment_array->comment_author_url) ? $comment_array->comment_author_url : 'javascript: void 0;', 120 | 'content' => $comment_array->comment_content, 121 | 'date' => $comment_array->comment_date, 122 | 'ua' => GetUserAgent::get_json($comment_array->comment_agent), 123 | 'location' => GetIpLocation::get_location($comment_array->comment_author_IP), 124 | 'level' => 6, 125 | 'role' => $role, 126 | 'like' => $like ? $like : 0, 127 | 'dislike' => $dislike ? $dislike : 0, 128 | 'child' => array( 129 | 'has_comment' => false, 130 | 'child_count' => null, 131 | 'preview_list' => array(), 132 | ), 133 | ); 134 | 135 | if ($this->show_preview) { 136 | // TOTO: how many to preview, and by what order 137 | $preview = new GetCommentChildList($comment_array->comment_ID, 3, 1); 138 | $preview_list = $preview->get_comments_formed_array(); 139 | $comment_formed_array['child'] = array_merge(array( 140 | 'has_comment' => $preview->has_comment, 141 | 'child_count' => $preview->comments_count, 142 | 'preview_list' => $preview->get_comments_formed_array(), 143 | )); 144 | $comment_formed_array['preview']; 145 | } 146 | 147 | array_push($comments_formed_array, $comment_formed_array); 148 | } 149 | 150 | return $comments_formed_array; 151 | } 152 | 153 | /** 154 | * Get comment count by $wpdb query, use WP_Comment_Query to get detail data. 155 | * But will this low down the performance? 156 | * @since 4.0 157 | * 158 | * @return array 159 | */ 160 | public function get_post_comments_count() 161 | { 162 | global $wpdb; 163 | 164 | $query = $wpdb->prepare( 165 | "SELECT COUNT(*) FROM {$wpdb->comments} 166 | WHERE comment_post_ID = %d 167 | AND comment_approved = %d 168 | AND comment_parent = %d 169 | ", $this->post_id, 1, 0 170 | ); 171 | 172 | $count = $wpdb->get_var($query); 173 | 174 | if ($count == 0) { 175 | $this->has_comment = false; 176 | } else { 177 | $this->has_comment = true; 178 | } 179 | 180 | return $count; 181 | } 182 | 183 | /** 184 | * calculate page offset and number 185 | * @since 4.0 186 | * 187 | * @return void 188 | */ 189 | public function calculate() 190 | { 191 | $post_id = $this->post_id; 192 | $page_size = $this->page_size; 193 | $target_page = $this->target_page; 194 | $comments_count = $this->comments_count; 195 | 196 | if ($comments_count % $page_size == 0) { 197 | $total_page = $comments_count / $page_size; 198 | } else { 199 | $total_page = intval($comments_count / $page_size) + 1; 200 | } 201 | 202 | if ($target_page >= $total_page && $comments_count % $page_size != 0) { 203 | $target_page = $total_page; 204 | $number = $comments_count % $page_size; 205 | } else { 206 | $number = $page_size; 207 | } 208 | 209 | $offset = $page_size * ($target_page - 1); 210 | 211 | $this->total_page = $total_page; 212 | $this->offset = $offset; 213 | $this->number = $number; 214 | 215 | return; 216 | } 217 | 218 | /** 219 | * Get comment by WP_Comment_Query 220 | * @since 4.0 221 | * 222 | * @return array 223 | */ 224 | public function get_post_comments() 225 | { 226 | $args = array( 227 | 'post_id' => $this->post_id, 228 | 'parent' => 0, 229 | 'orderby' => 'comment_ID', 230 | 'order' => 'ASC', 231 | 'number' => $this->number, 232 | 'offset' => $this->offset, 233 | ); 234 | // TODO: we can also make a top rated comments list by order by a 'meta_key' 235 | 236 | $comments_query = new WP_Comment_Query; 237 | $comments = $comments_query->query($args); 238 | return $comments; 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/php/classes/get-ip-location.php: -------------------------------------------------------------------------------- 1 | country->isoCode . "\n"); // 'US' 13 | * print($record->country->name . "\n"); // 'United States' 14 | * print($record->country->names['zh-CN'] . "\n"); // '美国' 15 | * 16 | * print($record->mostSpecificSubdivision->name . "\n"); // 'Minnesota' 17 | * print($record->mostSpecificSubdivision->isoCode . "\n"); // 'MN' 18 | * 19 | * print($record->city->name . "\n"); // 'Minneapolis' 20 | * 21 | * print($record->postal->code . "\n"); // '55455' 22 | * 23 | * print($record->location->latitude . "\n"); // 44.9733 24 | * print($record->location->longitude . "\n"); // -93.2323 25 | * 26 | * @param string $ip 27 | * @param string $lang = sakura_options('geo_ip_local') 28 | * @return string 29 | */ 30 | public static function get_ip_location_geoip2($ip, $lang = 'en') 31 | { 32 | $reader = new Reader(__DIR__ . '/../../data/GeoLite2-City.mmdb'); 33 | 34 | $record = $reader->city($ip); 35 | 36 | $location = $record->city->names[$lang] . ', '; 37 | 38 | // TOTO: 处理直辖市 39 | if (array_key_exists($lang, $record->mostSpecificSubdivision->names)) { 40 | $location .= $record->mostSpecificSubdivision->names[$lang] . ', '; 41 | } else { 42 | $location .= ''; 43 | } 44 | 45 | $location .= $record->country->names[$lang]; 46 | 47 | return $location; 48 | } 49 | 50 | /** 51 | * echo json_encode(QqIpLocation::getLocation('101.87.249.108'), JSON_UNESCAPED_UNICODE); 52 | * 53 | * @return string 54 | */ 55 | public static function get_ip_location_qqwarry($ip) 56 | { 57 | return QqIpLocation::getLocation($ip)['area']; 58 | } 59 | 60 | /** 61 | * UA string to readble string 62 | * @since 4.0 63 | * 64 | * @param string $ip; 65 | * @param string $db 1:geoip2 2:qqwarry 66 | * @return string 67 | */ 68 | public static function get_location($ip, $db = null) 69 | { 70 | $db = $db ? sakura_options('ip_database') : 1; 71 | try { 72 | if (sakura_options('ip_database') == 1) { 73 | return self::get_ip_location_geoip2($ip, sakura_options('geo_ip_local')); 74 | } else { 75 | return self::get_ip_location_qqwarry($ip); 76 | } 77 | } catch (Exception $e) { 78 | return ''; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/php/classes/get-user-agent.php: -------------------------------------------------------------------------------- 1 | 1) { 137 | $matches[1] = 'Lion ' . $matches[1]; 138 | } elseif (count(explode(8, $matches[1])) > 1) { 139 | $matches[1] = 'Mountain Lion ' . $matches[1]; 140 | } 141 | $text = $matches[0]; 142 | $icon = "android"; 143 | } elseif (preg_match('/Mac OS X.([0-9. _]+)/i', $ua, $matches)) { 144 | if (count(explode(7, $matches[1])) > 1) { 145 | $matches[1] = 'Lion ' . $matches[1]; 146 | } elseif (count(explode(8, $matches[1])) > 1) { 147 | $matches[1] = 'Mountain Lion ' . $matches[1]; 148 | } 149 | $text = "Mac OSX " . $matches[1]; 150 | $icon = "macos"; 151 | } elseif (preg_match('/Macintosh/i', $ua)) { 152 | $text = "Mac OS"; 153 | $icon = "macos"; 154 | } elseif (preg_match('/CrOS/i', $ua)) { 155 | $text = "Google Chrome OS"; 156 | $icon = "chrome"; 157 | } elseif (preg_match('/Linux/i', $ua)) { 158 | $text = 'Linux'; 159 | $icon = 'linux'; 160 | if (preg_match('/Android.([0-9. _]+)/i', $ua, $matches)) { 161 | $text = $matches[0]; 162 | $icon = "android"; 163 | } elseif (preg_match('#Ubuntu#i', $ua)) { 164 | $text = "Ubuntu Linux"; 165 | $icon = "ubuntu"; 166 | } elseif (preg_match('#Debian#i', $ua)) { 167 | $text = "Debian GNU/Linux"; 168 | $icon = "debian"; 169 | } elseif (preg_match('#Fedora#i', $ua)) { 170 | $text = "Fedora Linux"; 171 | $icon = "fedora"; 172 | } 173 | } 174 | return array( 175 | $text, 176 | $icon, 177 | ); 178 | } 179 | 180 | public static function get_html($ua) 181 | { 182 | $imgurl = 'https://cdn.jsdelivr.net/gh/moezx/cdn@3.2.7/img/Sakura/images/ua/svg/'; 183 | $browser = self::get_browsers($ua); 184 | $os = self::get_os($ua); 185 | return '(  ' . $browser[0] . '   ' . $os[0] . ' )'; 186 | } 187 | 188 | public static function get_json($ua) 189 | { 190 | $out = array(); 191 | array_push($out, array( 192 | 'os' => self::get_os($ua), 193 | 'browsers' => self::get_browsers($ua), 194 | )); 195 | return $out; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/php/classes/graphql-register-fields.php: -------------------------------------------------------------------------------- 1 | __("wow, a wonderful wordpress theme", 'sakrua'), 31 | 'fields' => [ 32 | 'name' => [ 33 | 'type' => 'String', 34 | 'description' => __('The name of the theme', 'sakrua'), 35 | ], 36 | 'author' => [ 37 | 'type' => 'String', 38 | 'description' => __('The author of the theme', 'sakrua'), 39 | ], 40 | 'version' => [ 41 | 'type' => 'String', 42 | 'description' => __('The version of the theme', 'sakrua'), 43 | ], 44 | ], 45 | ]); 46 | 47 | register_graphql_field('RootQuery', 'getTheme', [ 48 | 'description' => __('Get a theme', 'sakrua'), 49 | 'type' => 'Sakura', 50 | 'resolve' => function () { 51 | 52 | return [ 53 | 'name' => 'Sakura', 54 | 'author' => 'Mashiro', 55 | 'version' => wp_get_theme()->get('Version'), 56 | ]; 57 | 58 | }, 59 | ]); 60 | 61 | } 62 | 63 | /** 64 | * register a child list field 65 | * @since 4.0 66 | */ 67 | public static function register_comment_child_field() 68 | { 69 | register_graphql_field('RootQuery', 'commentChildListById', [ 70 | 'description' => __('Get comment child by database ID', 'sakura'), 71 | 'type' => 'commentList', 72 | 'args' => [ 73 | 'commentId' => [ 74 | 'type' => 'Int', 75 | 'description' => __('Get the comment by its database ID', 'sakura'), 76 | ], 77 | 'pageSize' => [ 78 | 'type' => 'Int', 79 | 'description' => __('How many comments to show in a page?', 'sakura'), 80 | ], 81 | 'targetPage' => [ 82 | 'type' => 'Int', 83 | 'description' => __('Which page of comments your want?', 'sakura'), 84 | ], 85 | ], 86 | 'resolve' => function ($source, $args, $context, $info) { 87 | $comment_child = new GetCommentChildList($args['commentId'], $args['pageSize'], $args['targetPage']); 88 | return [ 89 | 'wpDbId' => $comment_child->comment_id, 90 | 'pageSize' => $comment_child->page_size, 91 | 'totalPage' => $comment_child->total_page, 92 | 'targetPage' => $comment_child->target_page, 93 | 'comments' => json_encode($comment_child->get_comments_formed_array(), JSON_UNESCAPED_UNICODE), 94 | ]; 95 | 96 | }, 97 | ]); 98 | } 99 | 100 | /** 101 | * register a post comment list by postId field by 102 | * @since 4.0 103 | */ 104 | public static function register_comment_list_field() 105 | { 106 | /** 107 | * query by comment id in root query 108 | * @since 4.0 109 | */ 110 | register_graphql_field('RootQuery', 'commentListById', [ 111 | 'description' => __('Get comment child by database ID', 'sakura'), 112 | 'type' => 'commentList', 113 | 'args' => [ 114 | 'postId' => [ 115 | 'type' => 'Int', 116 | 'description' => __('Get the comment by post database ID', 'sakura'), 117 | ], 118 | 'pageSize' => [ 119 | 'type' => 'Int', 120 | 'description' => __('How many comments to show in a page?', 'sakura'), 121 | ], 122 | 'targetPage' => [ 123 | 'type' => 'Int', 124 | 'description' => __('Which page of comments your want?', 'sakura'), 125 | ], 126 | ], 127 | 'resolve' => function ($source, $args, $context, $info) { 128 | $comment_child = new GetCommentList($args['postId'], $args['pageSize'], $args['targetPage']); 129 | return [ 130 | 'wpDbId' => $comment_child->post_id, 131 | 'pageSize' => $comment_child->page_size, 132 | 'totalPage' => $comment_child->total_page, 133 | 'targetPage' => $comment_child->target_page, 134 | 'comments' => json_encode($comment_child->get_comments_formed_array(), JSON_UNESCAPED_UNICODE), 135 | ]; 136 | 137 | }, 138 | ]); 139 | } 140 | 141 | /** 142 | * Register connections to Comments 143 | * 144 | * @access public 145 | */ 146 | public static function register_connections() 147 | { 148 | /** 149 | * Register connection from RootQuery to Comments 150 | */ 151 | register_graphql_connection(self::get_connection_config()); 152 | 153 | /** 154 | * Register connection from Comment to children comments 155 | */ 156 | register_graphql_connection( 157 | self::get_connection_config( 158 | [ 159 | 'fromType' => 'commentByCommentId', 160 | 'fromFieldName' => 'children', 161 | ] 162 | ) 163 | ); 164 | } 165 | 166 | /** 167 | * Given an array of $args, this returns the connection config, merging the provided args 168 | * with the defaults 169 | * 170 | * @access public 171 | * 172 | * @param array $args 173 | * 174 | * @return array 175 | */ 176 | public static function get_connection_config($args = []) 177 | { 178 | $defaults = [ 179 | 'fromType' => 'RootQuery', 180 | 'toType' => 'commentItem', 181 | 'fromFieldName' => 'commentByCommentId', 182 | 'connectionArgs' => self::get_connection_args(), 183 | 'resolveNode' => function ($id, $args, $context, $info) { 184 | return DataSource::resolve_comment($id, $context); 185 | }, 186 | 'resolve' => function ($root, $args, $context, $info) { 187 | return DataSource::resolve_comments_connection($root, $args, $context, $info); 188 | }, 189 | ]; 190 | return array_merge($defaults, $args); 191 | } 192 | 193 | /** 194 | * This returns the connection args for the Comment connection 195 | * 196 | * @access public 197 | * @return array 198 | */ 199 | public static function get_connection_args() 200 | { 201 | return [ 202 | 'commentId' => [ 203 | 'type' => 'Integer', 204 | 'description' => __('The database id of the comment.', 'wp-graphql'), 205 | ], 206 | ]; 207 | } 208 | 209 | } 210 | -------------------------------------------------------------------------------- /src/php/classes/graphql-register-mutate.php: -------------------------------------------------------------------------------- 1 | [ 32 | 'comment_post_ID' => [ 33 | 'type' => 'Int', 34 | 'description' => __('The ID of the post that relates to the comment.', 'your-textdomain'), 35 | ], 36 | 'author' => [ 37 | 'type' => 'String', 38 | 'description' => __('The name of the comment author.', 'your-textdomain'), 39 | ], 40 | 'email' => [ 41 | 'type' => 'String', 42 | 'description' => __('The comment author email address.', 'your-textdomain'), 43 | ], 44 | 'url' => [ 45 | 'type' => 'String', 46 | 'description' => __('The comment author URL.', 'your-textdomain'), 47 | ], 48 | 'comment' => [ 49 | 'type' => 'String', 50 | 'description' => __('The content of the comment.', 'your-textdomain'), 51 | ], 52 | 'comment_parent' => [ 53 | 'type' => 'Int', 54 | 'description' => __('The ID of this comment\'s parent, if any. Default 0.', 'your-textdomain'), 55 | ], 56 | '_wp_unfiltered_html_comment' => [ 57 | 'type' => 'String', 58 | 'description' => __('The nonce value for allowing unfiltered HTML.', 'your-textdomain'), 59 | ], 60 | ], 61 | 62 | # outputFields expects an array of fields that can be asked for in response to the mutation 63 | # the resolve function is optional, but can be useful if the mutateAndPayload doesn't return an array 64 | # with the same key(s) as the outputFields 65 | 'outputFields' => [ 66 | 'succeed' => [ 67 | 'type' => 'Boolean', 68 | 'description' => __('True or false', 'your-textdomain'), 69 | 'resolve' => function ($payload, $args, $context, $info) { 70 | return isset($payload['succeed']) ? $payload['succeed'] : false; 71 | }, 72 | ], 73 | 'message' => [ 74 | 'type' => 'String', 75 | 'description' => __('Message', 'your-textdomain'), 76 | 'resolve' => function ($payload, $args, $context, $info) { 77 | return isset($payload['message']) ? $payload['message'] : null; 78 | }, 79 | ], 80 | 'comment_ID' => [ 81 | 'type' => 'Int', 82 | 'description' => __('You know what.', 'your-textdomain'), 83 | 'resolve' => function ($payload, $args, $context, $info) { 84 | return isset($payload['comment_ID']) ? $payload['comment_ID'] : null; 85 | }, 86 | ], 87 | 'comment_parent' => [ 88 | 'type' => 'Int', 89 | 'description' => __('You know what.', 'your-textdomain'), 90 | 'resolve' => function ($payload, $args, $context, $info) { 91 | return isset($payload['comment_parent']) ? $payload['comment_parent'] : null; 92 | }, 93 | ], 94 | 'comment_author' => [ 95 | 'type' => 'String', 96 | 'description' => __('You know what.', 'your-textdomain'), 97 | 'resolve' => function ($payload, $args, $context, $info) { 98 | return isset($payload['comment_author']) ? $payload['comment_author'] : null; 99 | }, 100 | ], 101 | 'comment_author_avatar' => [ 102 | 'type' => 'String', 103 | 'description' => __('You know what.', 'your-textdomain'), 104 | 'resolve' => function ($payload, $args, $context, $info) { 105 | return isset($payload['comment_author_avatar']) ? $payload['comment_author_avatar'] : null; 106 | }, 107 | ], 108 | 'url' => [ 109 | 'type' => 'String', 110 | 'description' => __('You know what.', 'your-textdomain'), 111 | 'resolve' => function ($payload, $args, $context, $info) { 112 | return isset($payload['url']) ? $payload['url'] : null; 113 | }, 114 | ], 115 | 'content' => [ 116 | 'type' => 'String', 117 | 'description' => __('You know what.', 'your-textdomain'), 118 | 'resolve' => function ($payload, $args, $context, $info) { 119 | return isset($payload['content']) ? $payload['content'] : null; 120 | }, 121 | ], 122 | 'date' => [ 123 | 'type' => 'String', 124 | 'description' => __('You know what.', 'your-textdomain'), 125 | 'resolve' => function ($payload, $args, $context, $info) { 126 | return isset($payload['date']) ? $payload['date'] : null; 127 | }, 128 | ], 129 | 'ua' => [ 130 | 'type' => 'String', 131 | 'description' => __('You know what.', 'your-textdomain'), 132 | 'resolve' => function ($payload, $args, $context, $info) { 133 | return isset($payload['ua']) ? $payload['ua'] : null; 134 | }, 135 | ], 136 | 'location' => [ 137 | 'type' => 'String', 138 | 'description' => __('You know what.', 'your-textdomain'), 139 | 'resolve' => function ($payload, $args, $context, $info) { 140 | return isset($payload['location']) ? $payload['location'] : null; 141 | }, 142 | ], 143 | 'level' => [ 144 | 'type' => 'Int', 145 | 'description' => __('You know what.', 'your-textdomain'), 146 | 'resolve' => function ($payload, $args, $context, $info) { 147 | return isset($payload['level']) ? $payload['level'] : null; 148 | }, 149 | ], 150 | 'role' => [ 151 | 'type' => 'String', 152 | 'description' => __('You know what.', 'your-textdomain'), 153 | 'resolve' => function ($payload, $args, $context, $info) { 154 | return isset($payload['role']) ? $payload['role'] : null; 155 | }, 156 | ], 157 | ], 158 | 159 | # mutateAndGetPayload expects a function, and the function gets passed the $input, $context, and $info 160 | # the function should return enough info for the outputFields to resolve with 161 | 'mutateAndGetPayload' => function ($input, $context, $info) { 162 | // Do any logic here to sanitize the input, check user capabilities, etc 163 | 164 | $comment_data = $input; 165 | if (empty($input['_wp_unfiltered_html_comment'])) { 166 | $comment_data['_wp_unfiltered_html_comment'] = ''; 167 | } 168 | $comment_handle = CommentsPostHandle::get_comment($comment_data); 169 | $comment = $comment_handle['comment']; 170 | $succeed = $comment_handle['succeed']; 171 | $message = $comment_handle['message']; 172 | 173 | if (property_exists($comment, 'user_id')) { 174 | $role = $comment->user_id ? get_userdata($comment->user_id)->roles[0] : 'visitor'; 175 | } else { 176 | $role = null; 177 | } 178 | 179 | // You'll get the url setting in profile page if you are loged in 180 | // So the url string might be a empty 181 | // https://core.trac.wordpress.org/browser/tags/5.3/src/wp-includes/comment.php#L3314 182 | if (property_exists($comment, 'comment_author_url')) { 183 | $url = $comment->comment_author_url; 184 | $url = !empty($url) ? $url : 'javascript: void 0;'; 185 | } else { 186 | $url = 'javascript: void 0;'; 187 | } 188 | 189 | return [ 190 | 'succeed' => $succeed, 191 | 'message' => $message, 192 | 'comment_ID' => property_exists($comment, 'comment_ID') ? $comment->comment_ID : null, 193 | 'comment_parent' => property_exists($comment, 'comment_parent') ? $comment->comment_parent : null, 194 | 'comment_author' => property_exists($comment, 'comment_author') ? $comment->comment_author : null, 195 | 'comment_author_avatar' => property_exists($comment, 'comment_author_email') ? get_avatar_url($comment->comment_author_email) : null, 196 | 'url' => $url, 197 | 'content' => property_exists($comment, 'comment_content') ? $comment->comment_content : null, 198 | 'date' => property_exists($comment, 'comment_date') ? $comment->comment_date : null, 199 | 'ua' => property_exists($comment, 'comment_agent') ? json_encode(GetUserAgent::get_json($comment->comment_agent)) : null, 200 | 'location' => property_exists($comment, 'comment_author_IP') ? GetIpLocation::get_location($comment->comment_author_IP) : null, 201 | 'level' => 6, 202 | 'role' => $role, 203 | ]; 204 | }, 205 | ]); 206 | 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/php/classes/graphql-register-types.php: -------------------------------------------------------------------------------- 1 | __("Get the comment list by its database ID or post database ID", 'sakura'), 21 | 'fields' => [ 22 | 'wpDbId' => [ 23 | 'type' => 'Integer', 24 | 'description' => __('The database id of the post/comment.', 'sakura'), 25 | ], 26 | 'pageSize' => [ 27 | 'type' => 'Integer', 28 | 'description' => __('How many comments to show in a page?', 'sakura'), 29 | ], 30 | 'totalPage' => [ 31 | 'type' => 'Integer', 32 | 'description' => __('How many page in total?', 'sakura'), 33 | ], 34 | 'targetPage' => [ 35 | 'type' => 'Integer', 36 | 'description' => __('Which page of comments your want?', 'sakura'), 37 | ], 38 | 'comments' => [ 39 | 'type' => 'String', 40 | 'description' => __('The comment list array, json encoded.', 'sakura'), 41 | ], 42 | ], 43 | ]); 44 | } 45 | 46 | /** 47 | * register commentMate type (drop) 48 | * @since 4.0 49 | */ 50 | public static function register_comment_mate_type() 51 | { 52 | register_graphql_object_type('commentByPostId', [ 53 | 'description' => __("Get the comment by its database ID", 'sakura'), 54 | 'fields' => [ 55 | 'postId' => [ 56 | 'type' => 'Integer', 57 | 'description' => __('The database id of the post', 'sakura'), 58 | ], 59 | ], 60 | ]); 61 | 62 | register_graphql_object_type('commentChildListById', [ 63 | 'description' => __("Get the comment by its database ID", 'sakura'), 64 | 'fields' => [ 65 | 'commentId' => [ 66 | 'type' => 'Integer', 67 | 'description' => __('The database id of the comment', 'sakura'), 68 | ], 69 | ], 70 | ]); 71 | 72 | register_graphql_object_type('commentItem', [ 73 | 'description' => __("Get the comment mate.", 'sakura'), 74 | 'fields' => [ 75 | 'authorName' => [ 76 | 'type' => 'String', 77 | 'description' => __('The name of the comment author', 'sakura'), 78 | ], 79 | 'authorAvatar' => [ 80 | 'type' => 'String', 81 | 'description' => __('The avatar of the comment author', 'sakura'), 82 | ], 83 | 'authorUrl' => [ 84 | 'type' => 'String', 85 | 'description' => __('The url of the comment author', 'sakura'), 86 | ], 87 | 'userAgent' => [ 88 | 'type' => 'String', 89 | 'description' => __('The user agent of the comment author', 'sakura'), 90 | ], 91 | 'location' => [ 92 | 'type' => 'String', 93 | 'description' => __('The IP location of the comment author', 'sakura'), 94 | ], 95 | 'level' => [ 96 | 'type' => 'Integer', 97 | 'description' => __('The level (by comment) of the comment author', 'sakura'), 98 | ], 99 | 'role' => [ 100 | 'type' => 'Integer', 101 | 'description' => __('The user role in wordpress of the comment author', 'sakura'), 102 | ], 103 | 'like' => [ 104 | 'type' => 'Integer', 105 | 'description' => __('The like count of the comment', 'sakura'), 106 | ], 107 | 'dislike' => [ 108 | 'type' => 'Integer', 109 | 'description' => __('The dislike count of the comment', 'sakura'), 110 | ], 111 | 'date' => [ 112 | 'type' => 'String', 113 | 'description' => __('Formated date of the comment', 'sakura'), 114 | ], 115 | 'content' => [ 116 | 'type' => 'String', 117 | 'description' => __('Content of the comment', 'sakura'), 118 | ], 119 | 'childCount' => [ 120 | 'type' => 'String', 121 | 'description' => __('The children ID of the comment', 'sakura'), 122 | ], 123 | 'childPreview' => [ 124 | 'type' => 'String', 125 | 'description' => __('The the first 3 children to show', 'sakura'), 126 | ], 127 | ], 128 | ]); 129 | 130 | register_graphql_object_type('pageNavigation', [ 131 | 'description' => __("The child comment detail list of the comment", 'sakura'), 132 | 'fields' => [ 133 | 'pageSize' => [ 134 | 'type' => 'Integer', 135 | 'description' => __('How many children you want in a query?', 'sakura'), 136 | ], 137 | 'targetPage' => [ 138 | 'type' => 'Integer', 139 | 'description' => __('Which page you are going to request?', 'sakura'), 140 | ], 141 | 'pageCount' => [ 142 | 'type' => 'Integer', 143 | 'description' => __('The total page number', 'sakura'), 144 | ], 145 | 'childCount' => [ 146 | 'type' => 'Integer', 147 | 'description' => __('How many child comments in total?', 'sakura'), 148 | ], 149 | ], 150 | ]); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/php/classes/lib/data-base-update.php: -------------------------------------------------------------------------------- 1 | array( 12 | 'cdn' => 'http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.mmdb.gz', 13 | 'github' => 'https://raw.githubusercontent.com/wp-statistics/GeoLite2-Country/master/GeoLite2-Country.mmdb.gz', 14 | 'file' => 'GeoLite2-Country', 15 | 'opt' => 'geoip' 16 | ), 17 | 'city' => array( 18 | 'cdn' => 'http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz', 19 | 'github' => 'https://raw.githubusercontent.com/wp-statistics/GeoLite2-City/master/GeoLite2-City.mmdb.gz', 20 | 'file' => 'GeoLite2-City', 21 | 'opt' => 'geoip_city' 22 | ) 23 | ); 24 | /** 25 | * Update option process. 26 | */ 27 | static function do_upgrade() { 28 | } 29 | /** 30 | * This function downloads the GeoIP database from MaxMind. 31 | * 32 | * @param $pack 33 | * @param string $type 34 | * 35 | * @return string 36 | */ 37 | static function download_geoip( $pack, $type = "enable" ) { 38 | GLOBAL $WP_Statistics; 39 | //Create Empty Return Function 40 | $result["status"] = false; 41 | // We need the download_url() function, it should exists on virtually all installs of PHP, but if it doesn't for some reason, bail out. 42 | if ( ! function_exists( 'download_url' ) ) { 43 | include( ABSPATH . 'wp-admin/includes/file.php' ); 44 | } 45 | // We need the wp_generate_password() function. 46 | if ( ! function_exists( 'wp_generate_password' ) ) { 47 | include( ABSPATH . 'wp-includes/pluggable.php' ); 48 | } 49 | // We need the gzopen() function, it should exists on virtually all installs of PHP, but if it doesn't for some reason, bail out. 50 | // Also stop trying to update the database as it just won't work :) 51 | if ( false === function_exists( 'gzopen' ) ) { 52 | if ( $type == "enable" ) { 53 | $WP_Statistics->update_option( self::$geoip[ $pack ]['opt'], '' ); 54 | } 55 | $result["notice"] = __( 'Error the gzopen() function do not exist!', 'wp-statistics' ); 56 | // TODO: add a admin error interface 57 | // WP_Statistics_Admin_Pages::set_admin_notice( $result["notice"], $type = 'error' ); 58 | return $result; 59 | } 60 | // If GeoIP is disabled, bail out. 61 | if ( $type == "update" and $WP_Statistics->get_option( self::$geoip[ $pack ]['opt'] ) == '' ) { 62 | return ''; 63 | } 64 | // This is the location of the file to download. 65 | $download_url = self::$geoip[ $pack ]['cdn']; 66 | $response = wp_remote_get( $download_url ); 67 | // Change download url if the maxmind.com doesn't response. 68 | if ( wp_remote_retrieve_response_code( $response ) != '200' ) { 69 | $download_url = self::$geoip[ $pack ]['github']; 70 | } 71 | // Get the upload directory from WordPress. 72 | $upload_dir = wp_upload_dir(); 73 | // Create a variable with the name of the database file to download. 74 | $DBFile = $upload_dir['basedir'] . '/wp-statistics/' . self::$geoip[ $pack ]['file'] . '.mmdb'; 75 | // Check to see if the subdirectory we're going to upload to exists, if not create it. 76 | if ( ! file_exists( $upload_dir['basedir'] . '/wp-statistics' ) ) { 77 | if ( ! @mkdir( $upload_dir['basedir'] . '/wp-statistics', 0755 ) ) { 78 | if ( $type == "enable" ) { 79 | $WP_Statistics->update_option( self::$geoip[ $pack ]['opt'], '' ); 80 | } 81 | $result["notice"] = sprintf( __( 'Error creating GeoIP database directory, make sure your web server has permissions to create directories in: %s', 'wp-statistics' ), $upload_dir['basedir'] ); 82 | WP_Statistics_Admin_Pages::set_admin_notice( $result["notice"], $type = 'error' ); 83 | return $result; 84 | } 85 | } 86 | if ( ! is_writable( $upload_dir['basedir'] . '/wp-statistics' ) ) { 87 | if ( $type == "enable" ) { 88 | $WP_Statistics->update_option( self::$geoip[ $pack ]['opt'], '' ); 89 | } 90 | $result["notice"] = sprintf( __( 'Error setting permissions of the GeoIP database directory, make sure your web server has permissions to write to directories in : %s', 'wp-statistics' ), 91 | $upload_dir['basedir'] 92 | ); 93 | WP_Statistics_Admin_Pages::set_admin_notice( $result["notice"], $type = 'error' ); 94 | return $result; 95 | } 96 | // Download the file from MaxMind, this places it in a temporary location. 97 | $TempFile = download_url( $download_url ); 98 | // If we failed, through a message, otherwise proceed. 99 | if ( is_wp_error( $TempFile ) ) { 100 | if ( $type == "enable" ) { 101 | $WP_Statistics->update_option( self::$geoip[ $pack ]['opt'], '' ); 102 | } 103 | $result["notice"] = sprintf( __( 'Error downloading GeoIP database from: %s - %s', 'wp-statistics' ), $download_url, $TempFile->get_error_message() ); 104 | WP_Statistics_Admin_Pages::set_admin_notice( $result["notice"], $type = 'error' ); 105 | } else { 106 | // Open the downloaded file to unzip it. 107 | $ZipHandle = gzopen( $TempFile, 'rb' ); 108 | // Create th new file to unzip to. 109 | $DBfh = fopen( $DBFile, 'wb' ); 110 | // If we failed to open the downloaded file, through an error and remove the temporary file. Otherwise do the actual unzip. 111 | if ( ! $ZipHandle ) { 112 | if ( $type == "enable" ) { 113 | $WP_Statistics->update_option( self::$geoip[ $pack ]['opt'], '' ); 114 | } 115 | $result["notice"] = sprintf( 116 | __( 'Error could not open downloaded GeoIP database for reading: %s', 'wp-statistics' ), 117 | $TempFile 118 | ); 119 | WP_Statistics_Admin_Pages::set_admin_notice( $result["notice"], $type = 'error' ); 120 | unlink( $TempFile ); 121 | } else { 122 | // If we failed to open the new file, throw and error and remove the temporary file. Otherwise actually do the unzip. 123 | if ( ! $DBfh ) { 124 | if ( $type == "enable" ) { 125 | $WP_Statistics->update_option( self::$geoip[ $pack ]['opt'], '' ); 126 | } 127 | $result["notice"] = sprintf( __( 'Error could not open destination GeoIP database for writing %s', 'wp-statistics' ), $DBFile ); 128 | WP_Statistics_Admin_Pages::set_admin_notice( $result["notice"], $type = 'error' ); 129 | unlink( $TempFile ); 130 | } else { 131 | while ( ( $data = gzread( $ZipHandle, 4096 ) ) != false ) { 132 | fwrite( $DBfh, $data ); 133 | } 134 | // Close the files. 135 | gzclose( $ZipHandle ); 136 | fclose( $DBfh ); 137 | // Delete the temporary file. 138 | unlink( $TempFile ); 139 | // Display the success message. 140 | $result["status"] = true; 141 | $result["notice"] = "

" . __( 'GeoIP Database updated successfully!', 'wp-statistics' ) . "

"; 142 | // Update the options to reflect the new download. 143 | if ( $type == "update" ) { 144 | $WP_Statistics->update_option( 'last_geoip_dl', time() ); 145 | $WP_Statistics->update_option( 'update_geoip', false ); 146 | } 147 | // Populate any missing GeoIP information if the user has selected the option. 148 | if ( $pack == "country" ) { 149 | if ( $WP_Statistics->get_option( 'geoip' ) && 150 | wp_statistics_geoip_supported() && 151 | $WP_Statistics->get_option( 'auto_pop' ) 152 | ) { 153 | self::populate_geoip_info(); 154 | } 155 | } 156 | } 157 | } 158 | } 159 | if ( $WP_Statistics->get_option( 'geoip_report' ) == true ) { 160 | $blogname = get_bloginfo( 'name' ); 161 | $blogemail = get_bloginfo( 'admin_email' ); 162 | $headers[] = "From: $blogname <$blogemail>"; 163 | $headers[] = "MIME-Version: 1.0"; 164 | $headers[] = "Content-type: text/html; charset=utf-8"; 165 | if ( $WP_Statistics->get_option( 'email_list' ) == '' ) { 166 | $WP_Statistics->update_option( 'email_list', $blogemail ); 167 | } 168 | wp_mail( $WP_Statistics->get_option( 'email_list' ), __( 'GeoIP update on', 'wp-statistics' ) . ' ' . WP_Statistics_Admin_Pages::sanitize_mail_subject( $blogname ), $result['notice'], $headers ); 169 | } 170 | // All of the messages displayed above are stored in a string, now it's time to actually output the messages. 171 | return $result; 172 | } 173 | /** 174 | * Downloads the referrer spam database from https://github.com/matomo-org/referrer-spam-blacklist. 175 | * @return string 176 | */ 177 | static function download_referrerspam() { 178 | global $WP_Statistics; 179 | // If referrer spam is disabled, bail out. 180 | if ( $WP_Statistics->get_option( 'referrerspam' ) == false ) { 181 | return ''; 182 | } 183 | // This is the location of the file to download. 184 | $download_url = 'https://raw.githubusercontent.com/matomo-org/referrer-spam-blacklist/master/spammers.txt'; 185 | // Download the file from MaxMind, this places it in a temporary location. 186 | $response = wp_remote_get( $download_url, array( 'timeout' => 30 ) ); 187 | if ( is_wp_error( $response ) ) { 188 | return false; 189 | } 190 | $referrerspamlist = wp_remote_retrieve_body( $response ); 191 | if ( is_wp_error( $referrerspamlist ) ) { 192 | return false; 193 | } 194 | if ( $referrerspamlist != '' || $WP_Statistics->get_option( 'referrerspamlist' ) != '' ) { 195 | $WP_Statistics->update_option( 'referrerspamlist', $referrerspamlist ); 196 | } 197 | return true; 198 | } 199 | /** 200 | * Populate GeoIP information in to the database. 201 | * It is used in two different parts of the plugin; 202 | * When a user manual requests the update to happen and after a new GeoIP database has been download 203 | * (if the option is selected). 204 | * 205 | * @return string 206 | */ 207 | static function populate_geoip_info() { 208 | global $wpdb, $WP_Statistics; 209 | // Find all rows in the table that currently don't have GeoIP info or have an unknown ('000') location. 210 | $result = $wpdb->get_results( "SELECT id,ip FROM `{$wpdb->prefix}statistics_visitor` WHERE location = '' or location = '000' or location IS NULL" ); 211 | // Try create a new reader instance. 212 | $reader = false; 213 | if ( $WP_Statistics->get_option( 'geoip' ) ) { 214 | $reader = $WP_Statistics::geoip_loader( 'country' ); 215 | } 216 | if ( $reader === false ) { 217 | $text_error = __( 'Unable to load the GeoIP database, make sure you have downloaded it in the settings page.', 'wp-statistics' ); 218 | WP_Statistics_Admin_Pages::set_admin_notice( $text_error, $type = 'error' ); 219 | } 220 | $count = 0; 221 | // Loop through all the missing rows and update them if we find a location for them. 222 | foreach ( $result as $item ) { 223 | $count ++; 224 | // If the IP address is only a hash, don't bother updating the record. 225 | if ( substr( $item->ip, 0, 6 ) != '#hash#' and $reader != false ) { 226 | try { 227 | $record = $reader->country( $item->ip ); 228 | $location = $record->country->isoCode; 229 | if ( $location == "" ) { 230 | $location = "000"; 231 | } 232 | } catch ( Exception $e ) { 233 | $location = "000"; 234 | } 235 | // Update the row in the database. 236 | $wpdb->update( 237 | $wpdb->prefix . "statistics_visitor", 238 | array( 'location' => $location ), 239 | array( 'id' => $item->id ) 240 | ); 241 | } 242 | } 243 | return "

" . sprintf( __( 'Updated %s GeoIP records in the visitors database.', 'wp-statistics' ), $count ) . "

"; 244 | } 245 | } -------------------------------------------------------------------------------- /src/php/classes/sakura-error.php: -------------------------------------------------------------------------------- 1 | esc_html('Drawer Menu (show on mobile, and not support sub-items)', 'sakura'), 37 | 'header-menu' => esc_html('Header Menu (show on desktap)', 'sakura'), 38 | 'header-sub-menu' => esc_html('Header Sub Menu (show on desktap)', 'sakura'), 39 | 'notice' => __('How to set your menus? Read this.', 'sakura'), 40 | )); 41 | } 42 | 43 | /** 44 | * Add theme support 45 | * @since 4.0.0 46 | */ 47 | public static function add_theme_support() 48 | { 49 | if (function_exists('add_theme_support')) { 50 | 51 | // Add Thumbnail Theme Support. 52 | add_theme_support('post-thumbnails'); 53 | add_image_size('large', 700, '', true); // Large Thumbnail. 54 | add_image_size('medium', 250, '', true); // Medium Thumbnail. 55 | add_image_size('small', 120, '', true); // Small Thumbnail. 56 | add_image_size('custom-size', 700, 200, true); // Custom Thumbnail Size call using the_post_thumbnail('custom-size'); 57 | 58 | // Add Support for Custom Backgrounds - Uncomment below if you're going to use. 59 | /*add_theme_support('custom-background', array( 60 | 'default-color' => 'FFF', 61 | 'default-image' => get_template_directory_uri() . '/img/bg.jpg' 62 | ));*/ 63 | 64 | // Add Support for Custom Header - Uncomment below if you're going to use. 65 | /*add_theme_support('custom-header', array( 66 | 'default-image' => get_template_directory_uri() . '/img/headers/default.jpg', 67 | 'header-text' => false, 68 | 'default-text-color' => '000', 69 | 'width' => 1000, 70 | 'height' => 198, 71 | 'random-default' => false, 72 | 'wp-head-callback' => $wphead_cb, 73 | 'admin-head-callback' => $adminhead_cb, 74 | 'admin-preview-callback' => $adminpreview_cb 75 | ));*/ 76 | 77 | // Enables post and comment RSS feed links to head. 78 | add_theme_support('automatic-feed-links'); 79 | 80 | // Enable HTML5 support. 81 | add_theme_support('html5', array('comment-list', 'comment-form', 'search-form', 'gallery', 'caption')); 82 | 83 | // Localisation Support. 84 | load_theme_textdomain('sakura', get_template_directory() . '/languages'); 85 | } 86 | } 87 | 88 | /** 89 | * Load scripts (header.php) 90 | * @since 4.0.0 91 | */ 92 | public static function sakura_header_scripts() 93 | { 94 | if ($GLOBALS['pagenow'] != 'wp-login.php' && !is_admin()) { 95 | // Remove jQuery 96 | wp_deregister_script('jquery'); 97 | 98 | wp_register_script('bundle-js', get_template_directory_uri() . join_paths('/', MANIFEST()['main']['js'][0]), array(), SAKURA_VERSION); 99 | wp_enqueue_script('bundle-js'); 100 | } 101 | } 102 | 103 | /** 104 | * Load styles 105 | * @since 4.0.0 106 | */ 107 | public static function sakura_styles() 108 | { 109 | // TODO add options! 110 | wp_deregister_style('wp-block-library'); // Gutenberg CSS 111 | 112 | // Icon fonts 113 | wp_register_style('MaterialIcons', 'https://fonts.googleapis.com/icon?family=Material+Icons', array(), SAKURA_VERSION); 114 | wp_enqueue_style('MaterialIcons'); 115 | 116 | wp_register_style('SakuraIcons', 'https://at.alicdn.com/t/font_679578_9p0ydgvimss.css', array(), SAKURA_VERSION); 117 | wp_enqueue_style('SakuraIcons'); 118 | 119 | wp_register_style('FontAwesome', 'https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css', array(), SAKURA_VERSION); 120 | wp_enqueue_style('FontAwesome'); 121 | 122 | // Custom CSS 123 | wp_register_style('sakura_style', get_template_directory_uri() . join_paths('/', MANIFEST()['main']['css'][0]), array(), SAKURA_VERSION); 124 | // Register CSS 125 | wp_enqueue_style('sakura_style'); 126 | } 127 | 128 | /** 129 | * Custom View Article link to Post 130 | */ 131 | public static function sakura_view_article($more) 132 | { 133 | global $post; 134 | return '... ' . esc_html_e('View Article', 'sakura') . ''; 135 | } 136 | 137 | /** 138 | * Remove Admin bar 139 | */ 140 | public static function remove_admin_bar() 141 | { 142 | return false; 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /src/php/classes/sakura-walker-drawer-nav.php: -------------------------------------------------------------------------------- 1 | display only for sub items. 21 | * 22 | * @since 3.0.0 23 | * 24 | * @see Walker::start_lvl() 25 | * 26 | * @param string $output Used to append additional content (passed by reference). 27 | * @param int $depth Depth of menu item. Used for padding. 28 | * @param stdClass $args An object of wp_nav_menu() arguments. 29 | */ 30 | public function start_lvl(&$output, $depth = 0, $args = null) 31 | { 32 | if (isset($args->item_spacing) && 'discard' === $args->item_spacing) { 33 | $t = ''; 34 | $n = ''; 35 | } else { 36 | $t = "\t"; 37 | $n = "\n"; 38 | } 39 | $indent = str_repeat($t, $depth); 40 | // Default class. 41 | $classes = array('sub-menu'); 42 | /** 43 | * Filters the CSS class(es) applied to a menu list element. 44 | * 45 | * @since 4.8.0 46 | * 47 | * @param string[] $classes Array of the CSS classes that are applied to the menu `{$n}"; 77 | } 78 | /** 79 | * Starts the element output. 80 | * 81 | * @since 3.0.0 82 | * @since 4.4.0 The {@see 'nav_menu_item_args'} filter was added. 83 | * 84 | * @see Walker::start_el() 85 | * 86 | * @param string $output Used to append additional content (passed by reference). 87 | * @param WP_Post $item Menu item data object. 88 | * @param int $depth Depth of menu item. Used for padding. 89 | * @param stdClass $args An object of wp_nav_menu() arguments. 90 | * @param int $id Current item ID. 91 | */ 92 | public function start_el(&$output, $item, $depth = 0, $args = null, $id = 0) 93 | { 94 | if (isset($args->item_spacing) && 'discard' === $args->item_spacing) { 95 | $t = ''; 96 | $n = ''; 97 | } else { 98 | $t = "\t"; 99 | $n = "\n"; 100 | } 101 | $indent = ($depth) ? str_repeat($t, $depth) : ''; 102 | $classes = empty($item->classes) ? array() : (array) $item->classes; 103 | $classes[] = 'menu-item-' . $item->ID; 104 | /** 105 | * Filters the arguments for a single nav menu item. 106 | * 107 | * @since 4.4.0 108 | * 109 | * @param stdClass $args An object of wp_nav_menu() arguments. 110 | * @param WP_Post $item Menu item data object. 111 | * @param int $depth Depth of menu item. Used for padding. 112 | */ 113 | $args = apply_filters('nav_menu_item_args', $args, $item, $depth); 114 | /** 115 | * Filters the CSS classes applied to a menu item's list item element. 116 | * 117 | * @since 3.0.0 118 | * @since 4.1.0 The `$depth` parameter was added. 119 | * 120 | * @param string[] $classes Array of the CSS classes that are applied to the menu item's `
  • ` element. 121 | * @param WP_Post $item The current menu item. 122 | * @param stdClass $args An object of wp_nav_menu() arguments. 123 | * @param int $depth Depth of menu item. Used for padding. 124 | */ 125 | $class_names = join(' ', apply_filters('nav_menu_css_class', array_filter($classes), $item, $args, $depth)); 126 | $class_names = $class_names ? ' class="' . esc_attr($class_names) . '"' : ''; 127 | /** 128 | * Filters the ID applied to a menu item's list item element. 129 | * 130 | * @since 3.0.1 131 | * @since 4.1.0 The `$depth` parameter was added. 132 | * 133 | * @param string $menu_id The ID that is applied to the menu item's `
  • ` element. 134 | * @param WP_Post $item The current menu item. 135 | * @param stdClass $args An object of wp_nav_menu() arguments. 136 | * @param int $depth Depth of menu item. Used for padding. 137 | */ 138 | $id = apply_filters('nav_menu_item_id', 'menu-item-' . $item->ID, $item, $args, $depth); 139 | $id = $id ? ' id="' . esc_attr($id) . '"' : ''; 140 | // $output .= $indent . ''; 141 | $atts = array(); 142 | $atts['title'] = !empty($item->attr_title) ? $item->attr_title : ''; 143 | $atts['target'] = !empty($item->target) ? $item->target : ''; 144 | if ('_blank' === $item->target && empty($item->xfn)) { 145 | $atts['rel'] = 'noopener noreferrer'; 146 | } else { 147 | $atts['rel'] = $item->xfn; 148 | } 149 | $atts['href'] = !empty($item->url) ? $item->url : ''; 150 | $atts['aria-current'] = $item->current ? 'page' : ''; 151 | /** 152 | * Filters the HTML attributes applied to a menu item's anchor element. 153 | * 154 | * @since 3.6.0 155 | * @since 4.1.0 The `$depth` parameter was added. 156 | * 157 | * @param array $atts { 158 | * The HTML attributes applied to the menu item's `` element, empty strings are ignored. 159 | * 160 | * @type string $title Title attribute. 161 | * @type string $target Target attribute. 162 | * @type string $rel The rel attribute. 163 | * @type string $href The href attribute. 164 | * @type string $aria_current The aria-current attribute. 165 | * } 166 | * @param WP_Post $item The current menu item. 167 | * @param stdClass $args An object of wp_nav_menu() arguments. 168 | * @param int $depth Depth of menu item. Used for padding. 169 | */ 170 | $atts = apply_filters('nav_menu_link_attributes', $atts, $item, $args, $depth); 171 | $attributes = ''; 172 | foreach ($atts as $attr => $value) { 173 | if (is_scalar($value) && '' !== $value && false !== $value) { 174 | $value = ('href' === $attr) ? esc_url($value) : esc_attr($value); 175 | $attributes .= ' ' . $attr . '="' . $value . '"'; 176 | } 177 | } 178 | /** This filter is documented in wp-includes/post-template.php */ 179 | $title = apply_filters('the_title', $item->title, $item->ID); 180 | /** 181 | * Filters a menu item's title. 182 | * 183 | * @since 4.4.0 184 | * 185 | * @param string $title The menu item's title. 186 | * @param WP_Post $item The current menu item. 187 | * @param stdClass $args An object of wp_nav_menu() arguments. 188 | * @param int $depth Depth of menu item. Used for padding. 189 | */ 190 | // HACK 191 | $output .= $indent . ''; 192 | $title = apply_filters('nav_menu_item_title', $title, $item, $args, $depth); 193 | preg_match('/\[(.*?)\](.*)/i', $title, $matches, PREG_UNMATCHED_AS_NULL); 194 | if (isset($matches[1])) { 195 | $icon = $matches[1]; 196 | $label = $matches[2]; 197 | $flag = 1; 198 | } else { 199 | preg_match('/\{(.*?)\}(.*)/i', $title, $match_iconfont, PREG_UNMATCHED_AS_NULL); 200 | if (isset($match_iconfont[1])) { 201 | $icon = $match_iconfont[1]; 202 | $label = $match_iconfont[2]; 203 | $flag = 2; 204 | } else { 205 | $label = $title; 206 | $flag = 3; 207 | } 208 | } 209 | if ($flag == 1) { 210 | $item_output = '' . $icon . ''; 211 | } elseif ($flag == 2) { 212 | $item_output = ''; 213 | } else { 214 | $item_output = ''; 215 | } 216 | $item_output .= ''; 217 | $item_output .= $label; 218 | $item_output .= ''; 219 | /** 220 | * Filters a menu item's starting output. 221 | * 222 | * The menu item's starting output only includes `$args->before`, the opening ``, 223 | * the menu item's title, the closing ``, and `$args->after`. Currently, there is 224 | * no filter for modifying the opening and closing `
  • ` for a menu item. 225 | * 226 | * @since 3.0.0 227 | * 228 | * @param string $item_output The menu item's starting HTML output. 229 | * @param WP_Post $item Menu item data object. 230 | * @param int $depth Depth of menu item. Used for padding. 231 | * @param stdClass $args An object of wp_nav_menu() arguments. 232 | */ 233 | $output .= apply_filters('walker_nav_menu_start_el', $item_output, $item, $depth, $args); 234 | } 235 | /** 236 | * Ends the element output, if needed. 237 | * 238 | * @since 3.0.0 239 | * 240 | * @see Walker::end_el() 241 | * 242 | * @param string $output Used to append additional content (passed by reference). 243 | * @param WP_Post $item Page data object. Not used. 244 | * @param int $depth Depth of page. Not Used. 245 | * @param stdClass $args An object of wp_nav_menu() arguments. 246 | */ 247 | public function end_el(&$output, $item, $depth = 0, $args = null) 248 | { 249 | if (isset($args->item_spacing) && 'discard' === $args->item_spacing) { 250 | $t = ''; 251 | $n = ''; 252 | } else { 253 | $t = "\t"; 254 | $n = "\n"; 255 | } 256 | // $output .= "
  • {$n}"; 257 | $output .= "{$n}"; 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/php/classes/sakura-walker-header-nav.php: -------------------------------------------------------------------------------- 1 | display only for sub items. 22 | * 23 | * @since 3.0.0 24 | * 25 | * @see Walker::start_lvl() 26 | * 27 | * @param string $output Used to append additional content (passed by reference). 28 | * @param int $depth Depth of menu item. Used for padding. 29 | * @param stdClass $args An object of wp_nav_menu() arguments. 30 | */ 31 | public function start_lvl(&$output, $depth = 0, $args = null) 32 | { 33 | if (isset($args->item_spacing) && 'discard' === $args->item_spacing) { 34 | $t = ''; 35 | $n = ''; 36 | } else { 37 | $t = "\t"; 38 | $n = "\n"; 39 | } 40 | $indent = str_repeat($t, $depth); 41 | // Default class. 42 | $classes = array('sub-menu', 'mdc-elevation--z1'); 43 | /** 44 | * Filters the CSS class(es) applied to a menu list element. 45 | * 46 | * @since 4.8.0 47 | * 48 | * @param string[] $classes Array of the CSS classes that are applied to the menu `