added additional plugins
This commit is contained in:
parent
c85895d306
commit
00e60ec1b7
132 changed files with 27514 additions and 0 deletions
112
native/wordpress/maple-code-blocks/.htaccess
Normal file
112
native/wordpress/maple-code-blocks/.htaccess
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
# GitHub Code Viewer Plugin - Security Rules
|
||||
|
||||
# Prevent directory browsing
|
||||
Options -Indexes
|
||||
|
||||
# Deny access to all files by default
|
||||
<FilesMatch ".*">
|
||||
Order Deny,Allow
|
||||
Deny from all
|
||||
</FilesMatch>
|
||||
|
||||
# Allow access to specific file types only
|
||||
<FilesMatch "\.(css|js|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)$">
|
||||
Order Allow,Deny
|
||||
Allow from all
|
||||
</FilesMatch>
|
||||
|
||||
# Specifically allow access to the main plugin file
|
||||
<Files "maple-code-blocks.php">
|
||||
Order Allow,Deny
|
||||
Allow from all
|
||||
</Files>
|
||||
|
||||
# Protect sensitive files
|
||||
<FilesMatch "(^\.|wp-config\.php|\.htaccess|\.htpasswd|error_log|readme\.html|license\.txt|install\.php|php\.ini|php5\.ini)">
|
||||
Order Allow,Deny
|
||||
Deny from all
|
||||
</FilesMatch>
|
||||
|
||||
# Disable PHP execution in subdirectories (except the root plugin file)
|
||||
<FilesMatch "\.php$">
|
||||
<If "%{REQUEST_URI} !~ m#^.*/maple-code-blocks/maple-code-blocks\.php$#">
|
||||
Order Deny,Allow
|
||||
Deny from all
|
||||
</If>
|
||||
</FilesMatch>
|
||||
|
||||
# Prevent script injection
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteBase /
|
||||
RewriteCond %{QUERY_STRING} (<|%3C).*script.*(>|%3E) [NC,OR]
|
||||
RewriteCond %{QUERY_STRING} GLOBALS(=|[|%[0-9A-Z]{0,2}) [OR]
|
||||
RewriteCond %{QUERY_STRING} _REQUEST(=|[|%[0-9A-Z]{0,2})
|
||||
RewriteRule ^(.*)$ - [F,L]
|
||||
</IfModule>
|
||||
|
||||
# Disable XML-RPC if not needed
|
||||
<Files xmlrpc.php>
|
||||
Order Deny,Allow
|
||||
Deny from all
|
||||
</Files>
|
||||
|
||||
# Add security headers
|
||||
<IfModule mod_headers.c>
|
||||
Header set X-Content-Type-Options "nosniff"
|
||||
Header set X-Frame-Options "SAMEORIGIN"
|
||||
Header set X-XSS-Protection "1; mode=block"
|
||||
Header set Referrer-Policy "strict-origin-when-cross-origin"
|
||||
</IfModule>
|
||||
|
||||
# Hotlinking protection disabled - not needed for WordPress plugins
|
||||
# WordPress plugins need their assets accessible to the host site
|
||||
# <IfModule mod_rewrite.c>
|
||||
# RewriteEngine on
|
||||
# RewriteCond %{HTTP_REFERER} !^$
|
||||
# RewriteCond %{HTTP_REFERER} !^https?://(www\.)?%{HTTP_HOST} [NC]
|
||||
# RewriteRule \.(css|js|png|jpg|jpeg|gif|svg)$ - [F,NC,L]
|
||||
# </IfModule>
|
||||
|
||||
# Compress text files
|
||||
<IfModule mod_deflate.c>
|
||||
AddOutputFilterByType DEFLATE text/plain
|
||||
AddOutputFilterByType DEFLATE text/html
|
||||
AddOutputFilterByType DEFLATE text/css
|
||||
AddOutputFilterByType DEFLATE application/javascript
|
||||
AddOutputFilterByType DEFLATE application/json
|
||||
</IfModule>
|
||||
|
||||
# Set proper MIME types
|
||||
<IfModule mod_mime.c>
|
||||
AddType text/css .css
|
||||
AddType application/javascript .js
|
||||
AddType application/json .json
|
||||
</IfModule>
|
||||
|
||||
# Cache control for static assets
|
||||
<IfModule mod_expires.c>
|
||||
ExpiresActive On
|
||||
ExpiresByType text/css "access plus 1 month"
|
||||
ExpiresByType application/javascript "access plus 1 month"
|
||||
ExpiresByType image/png "access plus 1 month"
|
||||
ExpiresByType image/jpg "access plus 1 month"
|
||||
ExpiresByType image/jpeg "access plus 1 month"
|
||||
ExpiresByType image/gif "access plus 1 month"
|
||||
ExpiresByType image/svg+xml "access plus 1 month"
|
||||
</IfModule>
|
||||
|
||||
# Disable server signature
|
||||
ServerSignature Off
|
||||
|
||||
# Prevent access to hidden files
|
||||
<FilesMatch "^\.">
|
||||
Order Allow,Deny
|
||||
Deny from all
|
||||
</FilesMatch>
|
||||
|
||||
# Block access to backup and source files
|
||||
<FilesMatch "(\.(bak|backup|config|dist|fla|inc|ini|log|psd|sh|sql|sw[op])|~)$">
|
||||
Order Allow,Deny
|
||||
Deny from all
|
||||
</FilesMatch>
|
||||
117
native/wordpress/maple-code-blocks/LICENSE
Normal file
117
native/wordpress/maple-code-blocks/LICENSE
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and modification follow.
|
||||
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
one line to give the program's name and an idea of what it does. Copyright (C) yyyy name of author
|
||||
|
||||
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice
|
||||
336
native/wordpress/maple-code-blocks/README.md
Normal file
336
native/wordpress/maple-code-blocks/README.md
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
# Maple Code Blocks WordPress Plugin
|
||||
|
||||
A beautiful and secure WordPress plugin that displays code files from GitHub repositories in a terminal/IDE-style interface with syntax highlighting.
|
||||
|
||||
## Features
|
||||
|
||||
✨ **Beautiful Interface**
|
||||
- Terminal/IDE-style code viewer
|
||||
- Dark and light themes
|
||||
- Syntax highlighting for 30+ languages
|
||||
- Line numbers
|
||||
- Tabbed interface for multiple files
|
||||
|
||||
🔒 **Security First**
|
||||
- All code is HTML-escaped (no XSS risk)
|
||||
- Code is displayed as text only, never executed
|
||||
- Multiple layers of content sanitization
|
||||
- Binary files automatically filtered
|
||||
- File size limits to prevent performance issues
|
||||
|
||||
🚀 **Functionality**
|
||||
- File browser with search
|
||||
- Copy code to clipboard
|
||||
- Fullscreen mode
|
||||
- Repository caching for performance
|
||||
- Responsive design
|
||||
- AJAX-powered for smooth experience
|
||||
|
||||
## Installation
|
||||
|
||||
1. Upload the `maple-code-blocks` folder to `/wp-content/plugins/`
|
||||
2. Activate the plugin through the 'Plugins' menu in WordPress
|
||||
3. Configure settings in Settings > Maple Code Blocks
|
||||
|
||||
# Maple Code Blocks - WordPress Plugin
|
||||
|
||||
A beautiful and secure WordPress plugin by **SSP Media** that displays code files from **GitHub, GitLab, Bitbucket, and Codeberg** repositories in a terminal/IDE-style interface with syntax highlighting. Now with **full Gutenberg block editor support**!
|
||||
|
||||
## About
|
||||
|
||||
**Maple Code Blocks** is developed and maintained by [SSP Media](https://sspmedia.ca/wordpress/), a Canadian web development agency specializing in WordPress solutions.
|
||||
|
||||
## Key Features
|
||||
|
||||
✅ **Public Access** - Visitors can view code without logging in (configurable)
|
||||
✅ **Multi-Platform** - Supports GitHub, GitLab, Bitbucket, and Codeberg
|
||||
✅ **Beautiful Themes** - Dark, Light, Monokai, Solarized
|
||||
✅ **Gutenberg Ready** - Full block editor support
|
||||
✅ **Secure** - Rate limiting, XSS protection, OWASP compliant
|
||||
|
||||
### Public Viewing Configuration
|
||||
|
||||
By default, all visitors can view public repository code. To restrict viewing to logged-in users only:
|
||||
|
||||
```php
|
||||
// Add to your theme's functions.php
|
||||
add_filter('mcb_require_login_for_viewing', '__return_true');
|
||||
```
|
||||
|
||||
## Supported Formats
|
||||
|
||||
### All Platforms Support These Formats:
|
||||
|
||||
#### 1. GitHub (default platform)
|
||||
```
|
||||
[maple_code_block repo="facebook/react"]
|
||||
[maple_code_block repo="github:facebook/react"]
|
||||
```
|
||||
|
||||
#### 2. GitLab
|
||||
```
|
||||
[maple_code_block repo="gitlab:gitlab-org/gitlab"]
|
||||
```
|
||||
|
||||
#### 3. Bitbucket
|
||||
```
|
||||
[maple_code_block repo="bitbucket:atlassian/python-bitbucket"]
|
||||
```
|
||||
|
||||
#### 4. Codeberg
|
||||
```
|
||||
[maple_code_block repo="codeberg:forgejo/forgejo"]
|
||||
```
|
||||
|
||||
### Format Guide:
|
||||
- **Simple format:** `owner/repo` (defaults to GitHub)
|
||||
- **Platform prefix:** `platform:owner/repo`
|
||||
- **Platforms:** `github`, `gitlab`, `bitbucket`, `codeberg`
|
||||
|
||||
### Full Examples with Options:
|
||||
|
||||
✅ **GitHub** - github.com
|
||||
✅ **GitLab** - gitlab.com
|
||||
✅ **Bitbucket** - bitbucket.org
|
||||
✅ **Codeberg** - codeberg.org
|
||||
|
||||
## Features
|
||||
|
||||
✨ **Beautiful Interface**
|
||||
- Terminal/IDE-style code viewer
|
||||
- Dark and light themes
|
||||
- Syntax highlighting for 30+ languages
|
||||
- Line numbers
|
||||
- Tabbed interface for multiple files
|
||||
- **NEW: Gutenberg block with live preview**
|
||||
- **NEW: Block variations and patterns**
|
||||
|
||||
🔒 **Security First**
|
||||
- All code is HTML-escaped (no XSS risk)
|
||||
- Code is displayed as text only, never executed
|
||||
- Multiple layers of content sanitization
|
||||
- Binary files automatically filtered
|
||||
- File size limits to prevent performance issues
|
||||
|
||||
🚀 **Functionality**
|
||||
- File browser with search
|
||||
- Copy code to clipboard
|
||||
- Fullscreen mode
|
||||
- Repository caching for performance
|
||||
- Responsive design
|
||||
- AJAX-powered for smooth experience
|
||||
- **NEW: Visual block editor**
|
||||
- **NEW: Pre-built block patterns**
|
||||
|
||||
## Installation
|
||||
|
||||
1. Upload the `maple-code-blocks` folder to `/wp-content/plugins/`
|
||||
2. Activate the plugin through the 'Plugins' menu in WordPress
|
||||
3. Configure settings in Settings > Maple Code Blocks
|
||||
|
||||
## Usage
|
||||
|
||||
### Method 1: Gutenberg Block (Recommended)
|
||||
|
||||
#### Using the Block Editor
|
||||
|
||||
1. In the WordPress block editor, click the "+" button to add a new block
|
||||
2. Search for "Maple Code Blocks"
|
||||
3. Select the block from the "Maple Code Blocks" category
|
||||
4. Configure the repository and settings in the block sidebar
|
||||
5. Preview your code viewer in real-time
|
||||
|
||||
#### Block Variations
|
||||
|
||||
The plugin includes pre-configured block variations:
|
||||
- **React Component**: Display React components
|
||||
- **Documentation Viewer**: Perfect for README files
|
||||
- **Full Repository Browser**: Browse entire repositories
|
||||
- **Code Snippet**: Display specific code files
|
||||
- **Tutorial Code**: Ideal for educational content
|
||||
|
||||
#### Block Patterns
|
||||
|
||||
Ready-to-use layouts available in the Pattern Library:
|
||||
- **Code with Explanation**: Code blocks with explanatory text
|
||||
- **Side-by-Side Comparison**: Compare two implementations
|
||||
- **Code Gallery**: Showcase multiple examples
|
||||
- **Featured Code**: Highlight important implementations
|
||||
- **Tutorial Steps**: Step-by-step code tutorials
|
||||
|
||||
#### Block Settings
|
||||
|
||||
Configure directly in the block editor sidebar:
|
||||
- **Repository**: Enter the GitHub repository (owner/repository)
|
||||
- **Theme**: Choose from Dark, Light, Monokai, or Solarized
|
||||
- **Height**: Set custom height (px, %, vh, em, rem)
|
||||
- **Line Numbers**: Toggle line numbers on/off
|
||||
- **Initial File**: Select a file to display on load
|
||||
- **Title**: Add an optional title
|
||||
- **Alignment**: Support for wide and full width
|
||||
|
||||
### Method 2: Classic Shortcode
|
||||
|
||||
#### Basic Shortcode
|
||||
|
||||
```
|
||||
[maple_code_block repo="owner/repository"]
|
||||
```
|
||||
|
||||
#### Platform-Specific Examples
|
||||
|
||||
**GitHub (default):**
|
||||
```
|
||||
[maple_code_block repo="facebook/react"]
|
||||
```
|
||||
|
||||
**GitLab:**
|
||||
```
|
||||
[maple_code_block repo="gitlab:gitlab-org/gitlab"]
|
||||
```
|
||||
|
||||
**Bitbucket:**
|
||||
```
|
||||
[maple_code_block repo="bitbucket:atlassian/python-bitbucket"]
|
||||
```
|
||||
|
||||
**Codeberg:**
|
||||
```
|
||||
[maple_code_block repo="codeberg:forgejo/forgejo"]
|
||||
```
|
||||
|
||||
**Using Full URLs:**
|
||||
```
|
||||
[maple_code_block repo="https://gitlab.com/fdroid/fdroidclient"]
|
||||
[maple_code_block repo="https://bitbucket.org/mailchimp/mandrill-api-php"]
|
||||
[maple_code_block repo="https://codeberg.org/forgejo/forgejo"]
|
||||
```
|
||||
|
||||
### Advanced Options
|
||||
|
||||
```
|
||||
[github_code_viewer
|
||||
repo="facebook/react"
|
||||
theme="dark"
|
||||
height="500px"
|
||||
show_line_numbers="true"
|
||||
initial_file="README.md"
|
||||
title="React Source Code"]
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `repo` | GitHub repository (owner/name) | Required |
|
||||
| `theme` | Color theme (dark/light/monokai/solarized) | dark |
|
||||
| `height` | Viewer height | 600px |
|
||||
| `show_line_numbers` | Show line numbers (true/false) | true |
|
||||
| `initial_file` | File to load initially | none |
|
||||
| `title` | Title above viewer | none |
|
||||
|
||||
## Security Features
|
||||
|
||||
### Multiple Layers of Protection
|
||||
|
||||
1. **Server-side Escaping**: All content is HTML-escaped using PHP's `htmlspecialchars()`
|
||||
2. **JavaScript Context Escaping**: Additional escaping for JavaScript contexts
|
||||
3. **Content Type Validation**: Binary files are automatically detected and rejected
|
||||
4. **Size Limits**: Files over 1MB are not displayed
|
||||
5. **Pattern Matching**: Dangerous patterns are replaced with safe alternatives
|
||||
6. **Safe Rendering**: Code is inserted as text content, never as HTML
|
||||
|
||||
### What This Means
|
||||
|
||||
- ✅ JavaScript code is displayed but NEVER executed
|
||||
- ✅ HTML tags are shown as text, not rendered
|
||||
- ✅ No possibility of XSS attacks
|
||||
- ✅ Safe to display any code from any public repository
|
||||
- ✅ WordPress site and database remain completely secure
|
||||
|
||||
## Configuration
|
||||
|
||||
### GitHub Personal Access Token (Optional)
|
||||
|
||||
For public repositories, no token is required. However, GitHub limits unauthenticated requests to 60 per hour. To increase this limit:
|
||||
|
||||
1. Go to Settings > Maple Code Blocks
|
||||
2. [Generate a GitHub token](https://github.com/settings/tokens) (no special permissions needed)
|
||||
3. Enter the token and save
|
||||
|
||||
### Cache Settings
|
||||
|
||||
Files are cached for 1 hour by default to improve performance. You can adjust this in the settings.
|
||||
|
||||
## Supported Languages
|
||||
|
||||
The plugin includes syntax highlighting for:
|
||||
|
||||
- JavaScript, TypeScript, JSX, TSX
|
||||
- Python, Ruby, PHP
|
||||
- Java, C, C++, C#
|
||||
- Go, Rust, Swift, Kotlin
|
||||
- HTML, CSS, SCSS, SASS, LESS
|
||||
- JSON, XML, YAML
|
||||
- SQL, Bash, Markdown
|
||||
- And many more...
|
||||
|
||||
## Performance
|
||||
|
||||
- **Caching**: Repository contents are cached to reduce API calls
|
||||
- **Lazy Loading**: Files are loaded on-demand
|
||||
- **Optimized Rendering**: Syntax highlighting is applied efficiently
|
||||
- **Responsive**: Works smoothly on all devices
|
||||
|
||||
## Styling
|
||||
|
||||
The plugin includes four built-in themes:
|
||||
|
||||
1. **Dark** - VS Code inspired dark theme
|
||||
2. **Light** - Clean light theme
|
||||
3. **Monokai** - Popular Sublime Text theme
|
||||
4. **Solarized** - Eye-friendly color scheme
|
||||
|
||||
## Browser Compatibility
|
||||
|
||||
- Chrome/Edge (latest)
|
||||
- Firefox (latest)
|
||||
- Safari (latest)
|
||||
- Opera (latest)
|
||||
|
||||
## Requirements
|
||||
|
||||
- WordPress 5.0 or higher
|
||||
- PHP 7.2 or higher
|
||||
- JavaScript enabled in browser
|
||||
|
||||
## Support
|
||||
|
||||
For issues, feature requests, or questions, please contact the plugin author or submit a support ticket.
|
||||
|
||||
## Support
|
||||
|
||||
For support, documentation, and updates, visit [SSP Media WordPress Plugins](https://sspmedia.ca/wordpress/).
|
||||
|
||||
## License
|
||||
|
||||
GPL v2 or later
|
||||
|
||||
---
|
||||
|
||||
**Maple Code Blocks** is proudly developed by [SSP Media](https://sspmedia.ca/wordpress/) 🍁
|
||||
|
||||
## Changelog
|
||||
|
||||
### Version 1.0.0
|
||||
- Initial release
|
||||
- Core functionality
|
||||
- Security features
|
||||
- Four themes
|
||||
- Syntax highlighting for 30+ languages
|
||||
|
||||
## Credits
|
||||
|
||||
- Syntax highlighting powered by a custom lightweight Prism.js implementation
|
||||
- Icons from various open-source icon sets
|
||||
- Inspired by VS Code and other modern code editors
|
||||
2
native/wordpress/maple-code-blocks/admin/index.php
Normal file
2
native/wordpress/maple-code-blocks/admin/index.php
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?php
|
||||
// Silence is golden.
|
||||
280
native/wordpress/maple-code-blocks/admin/settings-page.php
Normal file
280
native/wordpress/maple-code-blocks/admin/settings-page.php
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
<?php
|
||||
/**
|
||||
* Admin Settings Page
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('WPINC')) {
|
||||
die('Direct access not permitted.');
|
||||
}
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Additional security check
|
||||
if (!function_exists('current_user_can')) {
|
||||
die('WordPress environment not loaded.');
|
||||
}
|
||||
|
||||
// Check user capabilities
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_die('You do not have sufficient permissions to access this page.');
|
||||
}
|
||||
|
||||
// Save settings
|
||||
if (isset($_POST['mcb_save_settings'])) {
|
||||
// Verify nonce
|
||||
if (!isset($_POST['_wpnonce']) || !wp_verify_nonce($_POST['_wpnonce'], 'mcb_settings_nonce')) {
|
||||
wp_die('Security check failed');
|
||||
}
|
||||
|
||||
// Double-check capabilities
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_die('Insufficient permissions');
|
||||
}
|
||||
|
||||
// Save platform tokens
|
||||
$platforms = array('github', 'gitlab', 'bitbucket', 'codeberg');
|
||||
foreach ($platforms as $platform) {
|
||||
$token_field = 'mcb_' . $platform . '_token';
|
||||
$token = sanitize_text_field($_POST[$token_field] ?? '');
|
||||
update_option($token_field, $token);
|
||||
}
|
||||
|
||||
// Save cache duration
|
||||
$cache_duration = absint($_POST['mcb_cache_duration']);
|
||||
update_option('mcb_cache_duration', $cache_duration);
|
||||
|
||||
// Save default theme
|
||||
$default_theme = sanitize_text_field($_POST['mcb_default_theme']);
|
||||
update_option('mcb_default_theme', $default_theme);
|
||||
|
||||
echo '<div class="notice notice-success"><p>Settings saved successfully!</p></div>';
|
||||
}
|
||||
|
||||
// Get current settings
|
||||
$github_token = get_option('mcb_github_token', '');
|
||||
$gitlab_token = get_option('mcb_gitlab_token', '');
|
||||
$bitbucket_token = get_option('mcb_bitbucket_token', '');
|
||||
$codeberg_token = get_option('mcb_codeberg_token', '');
|
||||
$cache_duration = get_option('mcb_cache_duration', 3600);
|
||||
$default_theme = get_option('mcb_default_theme', 'dark');
|
||||
?>
|
||||
|
||||
<div class="wrap">
|
||||
<h1>Maple Code Blocks Settings</h1>
|
||||
<p>Configure your Maple Code Blocks plugin settings below. Developed by <a href="https://sspmedia.ca/wordpress/" target="_blank">SSP Media</a>.</p>
|
||||
|
||||
<form method="post" action="">
|
||||
<?php wp_nonce_field('mcb_settings_nonce'); ?>
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th colspan="2">
|
||||
<h2>Platform API Tokens</h2>
|
||||
<p>Optional: Add personal access tokens to increase API rate limits for each platform.</p>
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="mcb_github_token">GitHub Token</label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" id="mcb_github_token" name="mcb_github_token"
|
||||
value="<?php echo esc_attr($github_token); ?>" class="regular-text" />
|
||||
<p class="description">
|
||||
<a href="https://github.com/settings/tokens" target="_blank">Generate GitHub token</a>
|
||||
(no special permissions needed for public repos)
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="mcb_gitlab_token">GitLab Token</label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" id="mcb_gitlab_token" name="mcb_gitlab_token"
|
||||
value="<?php echo esc_attr($gitlab_token); ?>" class="regular-text" />
|
||||
<p class="description">
|
||||
<a href="https://gitlab.com/-/profile/personal_access_tokens" target="_blank">Generate GitLab token</a>
|
||||
(read_api scope for public repos)
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="mcb_bitbucket_token">Bitbucket App Password</label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" id="mcb_bitbucket_token" name="mcb_bitbucket_token"
|
||||
value="<?php echo esc_attr($bitbucket_token); ?>" class="regular-text" />
|
||||
<p class="description">
|
||||
<a href="https://bitbucket.org/account/settings/app-passwords/" target="_blank">Generate Bitbucket app password</a>
|
||||
(repository read permission)
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="mcb_codeberg_token">Codeberg Token</label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" id="mcb_codeberg_token" name="mcb_codeberg_token"
|
||||
value="<?php echo esc_attr($codeberg_token); ?>" class="regular-text" />
|
||||
<p class="description">
|
||||
<a href="https://codeberg.org/user/settings/applications" target="_blank">Generate Codeberg token</a>
|
||||
(read:repository scope)
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th colspan="2">
|
||||
<h2>General Settings</h2>
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="mcb_cache_duration">Cache Duration</label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" id="mcb_cache_duration" name="mcb_cache_duration"
|
||||
value="<?php echo esc_attr($cache_duration); ?>" min="0" /> seconds
|
||||
<p class="description">
|
||||
How long to cache repository files (default: 3600 seconds = 1 hour).<br>
|
||||
Set to 0 to disable caching.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="mcb_default_theme">Default Theme</label>
|
||||
</th>
|
||||
<td>
|
||||
<select id="mcb_default_theme" name="mcb_default_theme">
|
||||
<option value="dark" <?php selected($default_theme, 'dark'); ?>>Dark</option>
|
||||
<option value="light" <?php selected($default_theme, 'light'); ?>>Light</option>
|
||||
<option value="monokai" <?php selected($default_theme, 'monokai'); ?>>Monokai</option>
|
||||
<option value="solarized" <?php selected($default_theme, 'solarized'); ?>>Solarized</option>
|
||||
</select>
|
||||
<p class="description">
|
||||
Default theme for the code viewer. Can be overridden per shortcode.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p class="submit">
|
||||
<input type="submit" name="mcb_save_settings" class="button-primary" value="Save Settings" />
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<hr />
|
||||
|
||||
<h2>Usage Instructions</h2>
|
||||
|
||||
<div class="card">
|
||||
<h3>Supported Platforms</h3>
|
||||
<p>This plugin supports repositories from:</p>
|
||||
<ul>
|
||||
<li><strong>GitHub</strong> - github.com</li>
|
||||
<li><strong>GitLab</strong> - gitlab.com</li>
|
||||
<li><strong>Bitbucket</strong> - bitbucket.org</li>
|
||||
<li><strong>Codeberg</strong> - codeberg.org</li>
|
||||
</ul>
|
||||
|
||||
<h3>Repository Formats</h3>
|
||||
<p>You can specify repositories in multiple ways:</p>
|
||||
<ul>
|
||||
<li><strong>Default (GitHub):</strong> <code>owner/repository</code></li>
|
||||
<li><strong>With platform prefix:</strong> <code>gitlab:owner/repository</code></li>
|
||||
<li><strong>Full URL:</strong> <code>https://gitlab.com/owner/repository</code></li>
|
||||
</ul>
|
||||
|
||||
<h3>Basic Usage</h3>
|
||||
<p>Use the following shortcode to display repository code:</p>
|
||||
<code>[maple_code_block repo="owner/repository"]</code>
|
||||
|
||||
<h3>Platform Examples</h3>
|
||||
<ul>
|
||||
<li><strong>GitHub:</strong> <code>[maple_code_block repo="facebook/react"]</code></li>
|
||||
<li><strong>GitLab:</strong> <code>[maple_code_block repo="gitlab:gitlab-org/gitlab"]</code></li>
|
||||
<li><strong>Bitbucket:</strong> <code>[maple_code_block repo="bitbucket:atlassian/python-bitbucket"]</code></li>
|
||||
<li><strong>Codeberg:</strong> <code>[maple_code_block repo="codeberg:forgejo/forgejo"]</code></li>
|
||||
</ul>
|
||||
|
||||
<h3>Shortcode Parameters</h3>
|
||||
<ul>
|
||||
<li><strong>repo</strong> - (required) The GitHub repository in format "owner/repository"</li>
|
||||
<li><strong>theme</strong> - Theme: dark, light, monokai, or solarized (default: dark)</li>
|
||||
<li><strong>height</strong> - Height of the viewer (default: 600px)</li>
|
||||
<li><strong>show_line_numbers</strong> - Show line numbers: true or false (default: true)</li>
|
||||
<li><strong>initial_file</strong> - Path to file to load initially</li>
|
||||
<li><strong>title</strong> - Optional title to display above the viewer</li>
|
||||
</ul>
|
||||
|
||||
<h3>Example</h3>
|
||||
<code>[github_code_viewer repo="facebook/react" theme="dark" height="500px" initial_file="README.md" title="React Source Code"]</code>
|
||||
|
||||
<h3>Security Features</h3>
|
||||
<ul>
|
||||
<li>✅ All code is HTML-escaped to prevent XSS attacks</li>
|
||||
<li>✅ JavaScript code is displayed as text only, never executed</li>
|
||||
<li>✅ Binary files are automatically filtered out</li>
|
||||
<li>✅ File size limits prevent loading huge files</li>
|
||||
<li>✅ Content is sanitized multiple times before display</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="card">
|
||||
<h3>About</h3>
|
||||
<p>
|
||||
<strong>GitHub Code Viewer</strong> Version 1.0.0<br>
|
||||
This plugin displays code from GitHub repositories in a beautiful, safe terminal/IDE-style interface.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Features:</strong>
|
||||
</p>
|
||||
<ul>
|
||||
<li>Beautiful syntax highlighting with Prism.js</li>
|
||||
<li>Terminal/IDE-style interface</li>
|
||||
<li>File browser with search</li>
|
||||
<li>Tabbed interface for multiple files</li>
|
||||
<li>Copy code functionality</li>
|
||||
<li>Line numbers</li>
|
||||
<li>Multiple themes</li>
|
||||
<li>Fullscreen mode</li>
|
||||
<li>Responsive design</li>
|
||||
<li>Safe display (no code execution)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
background: white;
|
||||
border: 1px solid #ccd0d4;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.card h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
.card code {
|
||||
display: block;
|
||||
padding: 10px;
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
</style>
|
||||
292
native/wordpress/maple-code-blocks/assets/css/block-editor.css
Normal file
292
native/wordpress/maple-code-blocks/assets/css/block-editor.css
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
/**
|
||||
* GitHub Code Viewer Block Editor Styles
|
||||
*/
|
||||
|
||||
/* Block Editor Wrapper */
|
||||
.mcb-block-editor {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
}
|
||||
|
||||
/* Block Preview */
|
||||
.mcb-block-preview {
|
||||
min-height: 200px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.mcb-block-preview:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Preview Header */
|
||||
.mcb-preview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.mcb-preview-header svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Preview Info Grid */
|
||||
.mcb-preview-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.mcb-preview-info > div {
|
||||
padding: 5px 10px;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.editor-styles-wrapper .mcb-block-preview.theme-dark .mcb-preview-info > div {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Inspector Controls Customization */
|
||||
.mcb-height-control {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.mcb-height-control label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Popular Repos Buttons */
|
||||
.mcb-popular-repos {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* Validation States */
|
||||
.mcb-validation-success {
|
||||
color: #00a32a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.mcb-validation-error {
|
||||
color: #cc1818;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.mcb-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.mcb-loading .components-spinner {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/* Placeholder Styles */
|
||||
.wp-block-maple-code-blocks-code-viewer .components-placeholder {
|
||||
min-height: 200px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.wp-block-maple-code-blocks-code-viewer .components-placeholder .components-placeholder__label {
|
||||
font-size: 18px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.wp-block-maple-code-blocks-code-viewer .components-placeholder .components-placeholder__instructions {
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Quick Start Section */
|
||||
.mcb-quick-start {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.mcb-quick-start strong {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.mcb-quick-start .components-button {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
/* Block Alignment Support */
|
||||
.wp-block-maple-code-blocks-code-viewer.alignwide {
|
||||
max-width: 1280px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.wp-block-maple-code-blocks-code-viewer.alignfull {
|
||||
max-width: none;
|
||||
margin-left: calc(50% - 50vw);
|
||||
margin-right: calc(50% - 50vw);
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
/* Panel Body Customization */
|
||||
.components-panel__body .mcb-repo-input {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.components-panel__body .mcb-theme-preview {
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin-top: 10px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.components-panel__body .mcb-theme-preview.dark {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.components-panel__body .mcb-theme-preview.light {
|
||||
background: #ffffff;
|
||||
color: #333333;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
/* External Link Style */
|
||||
.components-external-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Toolbar Buttons */
|
||||
.block-editor-block-toolbar .mcb-toolbar-group {
|
||||
border-right: 1px solid #e0e0e0;
|
||||
padding-right: 6px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
/* Notice Improvements */
|
||||
.components-notice.mcb-notice {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.components-notice.mcb-notice .components-notice__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Selected State */
|
||||
.wp-block-maple-code-blocks-code-viewer.is-selected .mcb-block-preview {
|
||||
box-shadow: 0 0 0 1px #007cba;
|
||||
}
|
||||
|
||||
/* Help Text */
|
||||
.components-base-control__help {
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
color: #757575;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* File Selector */
|
||||
.mcb-file-selector {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
padding: 5px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mcb-file-selector .mcb-file-option {
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.mcb-file-selector .mcb-file-option:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.mcb-file-selector .mcb-file-option.selected {
|
||||
background: #007cba;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 782px) {
|
||||
.mcb-preview-info {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.mcb-quick-start .components-button {
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark Theme Support for Editor */
|
||||
.editor-styles-wrapper .mcb-block-preview[data-theme="dark"] {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.editor-styles-wrapper .mcb-block-preview[data-theme="light"] {
|
||||
background: #ffffff;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
/* Animation for validation */
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.mcb-validating {
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
/* Custom scrollbar for file list */
|
||||
.mcb-file-selector::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.mcb-file-selector::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
.mcb-file-selector::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.mcb-file-selector::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
202
native/wordpress/maple-code-blocks/assets/css/block-style.css
Normal file
202
native/wordpress/maple-code-blocks/assets/css/block-style.css
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
/**
|
||||
* GitHub Code Viewer Block Frontend Styles
|
||||
*/
|
||||
|
||||
/* Block Wrapper */
|
||||
.mcb-block-wrapper {
|
||||
margin: 30px auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Alignment Support */
|
||||
.mcb-block-wrapper.alignleft {
|
||||
float: left;
|
||||
margin-right: 20px;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.mcb-block-wrapper.alignright {
|
||||
float: right;
|
||||
margin-left: 20px;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.mcb-block-wrapper.aligncenter {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mcb-block-wrapper.aligncenter .maple-code-blocks {
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mcb-block-wrapper.alignwide {
|
||||
max-width: 1280px;
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.mcb-block-wrapper.alignfull {
|
||||
max-width: none;
|
||||
width: 100vw;
|
||||
position: relative;
|
||||
left: 50%;
|
||||
right: 50%;
|
||||
margin-left: -50vw;
|
||||
margin-right: -50vw;
|
||||
}
|
||||
|
||||
/* Ensure the viewer respects container width */
|
||||
.mcb-block-wrapper .maple-code-blocks {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Custom Classes Support */
|
||||
.mcb-block-wrapper.is-style-minimal {
|
||||
box-shadow: none;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.mcb-block-wrapper.is-style-rounded .maple-code-blocks {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mcb-block-wrapper.is-style-shadowed .maple-code-blocks {
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Responsive Adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.mcb-block-wrapper.alignleft,
|
||||
.mcb-block-wrapper.alignright {
|
||||
float: none;
|
||||
max-width: 100%;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.mcb-block-wrapper.alignfull {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading State for Frontend */
|
||||
.mcb-block-wrapper.is-loading {
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.mcb-block-wrapper.is-loading::after {
|
||||
content: 'Loading repository...';
|
||||
color: #666;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
/* Error State */
|
||||
.mcb-block-wrapper .mcb-error {
|
||||
padding: 20px;
|
||||
background: #fee;
|
||||
border: 1px solid #fcc;
|
||||
border-radius: 4px;
|
||||
color: #c00;
|
||||
text-align: center;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
/* Integration with theme styles */
|
||||
.entry-content .mcb-block-wrapper {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.entry-content .mcb-block-wrapper:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.entry-content .mcb-block-wrapper:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
.mcb-block-wrapper .mcb-controls,
|
||||
.mcb-block-wrapper .mcb-status-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mcb-block-wrapper .maple-code-blocks {
|
||||
box-shadow: none;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
/* Accessibility Improvements */
|
||||
.mcb-block-wrapper .maple-code-blocks:focus-within {
|
||||
outline: 2px solid #007cba;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* High Contrast Mode Support */
|
||||
@media (prefers-contrast: high) {
|
||||
.mcb-block-wrapper .maple-code-blocks {
|
||||
border: 2px solid currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced Motion Support */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.mcb-block-wrapper .maple-code-blocks * {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark Mode Support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.mcb-block-wrapper.is-loading {
|
||||
background: #2a2a2a;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.mcb-block-wrapper.is-loading::after {
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
/* Nested Block Support */
|
||||
.wp-block-group .mcb-block-wrapper,
|
||||
.wp-block-column .mcb-block-wrapper {
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
/* Pattern Library Support */
|
||||
.mcb-block-wrapper[data-pattern="documentation"] .maple-code-blocks {
|
||||
height: 400px !important;
|
||||
}
|
||||
|
||||
.mcb-block-wrapper[data-pattern="showcase"] .maple-code-blocks {
|
||||
height: 600px !important;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.mcb-block-wrapper[data-pattern="inline"] .maple-code-blocks {
|
||||
height: 300px !important;
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
min-width: 500px;
|
||||
}
|
||||
2
native/wordpress/maple-code-blocks/assets/css/index.php
Normal file
2
native/wordpress/maple-code-blocks/assets/css/index.php
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?php
|
||||
// Silence is golden.
|
||||
529
native/wordpress/maple-code-blocks/assets/css/mcb-styles.css
Normal file
529
native/wordpress/maple-code-blocks/assets/css/mcb-styles.css
Normal file
|
|
@ -0,0 +1,529 @@
|
|||
/* GitHub Code Viewer - Main Styles */
|
||||
|
||||
.maple-code-blocks {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
background: #1e1e1e;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Dark Theme (Default) */
|
||||
.maple-code-blocks[data-theme="dark"],
|
||||
.maple-code-blocks.mcb-theme-dark {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.maple-code-blocks[data-theme="dark"] .mcb-header,
|
||||
.maple-code-blocks.mcb-theme-dark .mcb-header {
|
||||
background: linear-gradient(180deg, #2d2d30 0%, #252526 100%);
|
||||
border-bottom: 1px solid #3e3e42;
|
||||
}
|
||||
|
||||
.maple-code-blocks[data-theme="dark"] .mcb-sidebar,
|
||||
.maple-code-blocks.mcb-theme-dark .mcb-sidebar {
|
||||
background: #252526;
|
||||
border-right: 1px solid #3e3e42;
|
||||
}
|
||||
|
||||
.maple-code-blocks[data-theme="dark"] .mcb-editor,
|
||||
.maple-code-blocks.mcb-theme-dark .mcb-editor {
|
||||
background: #1e1e1e;
|
||||
}
|
||||
|
||||
/* Light Theme */
|
||||
.maple-code-blocks[data-theme="light"],
|
||||
.maple-code-blocks.mcb-theme-light {
|
||||
background: #ffffff;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.maple-code-blocks[data-theme="light"] .mcb-header,
|
||||
.maple-code-blocks.mcb-theme-light .mcb-header {
|
||||
background: linear-gradient(180deg, #f3f3f3 0%, #e8e8e8 100%);
|
||||
border-bottom: 1px solid #d4d4d4;
|
||||
}
|
||||
|
||||
.maple-code-blocks[data-theme="light"] .mcb-sidebar,
|
||||
.maple-code-blocks.mcb-theme-light .mcb-sidebar {
|
||||
background: #f3f3f3;
|
||||
border-right: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.maple-code-blocks[data-theme="light"] .mcb-editor,
|
||||
.maple-code-blocks.mcb-theme-light .mcb-editor {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.mcb-header {
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.mcb-title {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.mcb-repo-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.mcb-github-icon,
|
||||
.mcb-platform-icon {
|
||||
fill: currentColor;
|
||||
opacity: 0.7;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.mcb-repo-name {
|
||||
font-size: 13px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.mcb-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mcb-controls button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.mcb-controls button:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.mcb-controls button svg {
|
||||
fill: currentColor;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Content Area */
|
||||
.mcb-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.mcb-sidebar {
|
||||
width: 240px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mcb-search-box {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.mcb-search-input {
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
color: inherit;
|
||||
font-size: 12px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.mcb-search-input:focus {
|
||||
border-color: #007acc;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* File List */
|
||||
.mcb-file-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.mcb-file-item {
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
transition: background-color 0.1s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.mcb-file-item:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.mcb-file-item.active {
|
||||
background: rgba(0, 122, 204, 0.3);
|
||||
}
|
||||
|
||||
.mcb-file-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mcb-file-name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mcb-file-size {
|
||||
font-size: 11px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Folder styles */
|
||||
.mcb-folder .mcb-file-icon,
|
||||
.mcb-parent-folder .mcb-file-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.mcb-folder {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mcb-folder:hover,
|
||||
.mcb-parent-folder:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.mcb-parent-folder {
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding-bottom: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mcb-breadcrumb {
|
||||
padding: 8px 12px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
font-size: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mcb-path-label {
|
||||
opacity: 0.6;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.mcb-current-path {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* Editor Area */
|
||||
.mcb-editor {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.mcb-tabs {
|
||||
display: flex;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
overflow-x: auto;
|
||||
min-height: 35px;
|
||||
}
|
||||
|
||||
.mcb-tab {
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.05);
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mcb-tab:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.mcb-tab.active {
|
||||
background: var(--editor-bg, #1e1e1e);
|
||||
border-bottom: 2px solid #007acc;
|
||||
}
|
||||
|
||||
.mcb-tab-close {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.mcb-tab-close:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Code Area */
|
||||
.mcb-code-area {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mcb-welcome {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.mcb-welcome svg {
|
||||
fill: currentColor;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mcb-welcome h4 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.mcb-welcome p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Code Container */
|
||||
.mcb-code-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mcb-code-header {
|
||||
padding: 8px 16px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.mcb-filename {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mcb-copy-btn {
|
||||
padding: 4px 12px;
|
||||
background: rgba(0, 122, 204, 0.2);
|
||||
border: 1px solid #007acc;
|
||||
border-radius: 4px;
|
||||
color: #007acc;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.mcb-copy-btn:hover {
|
||||
background: rgba(0, 122, 204, 0.3);
|
||||
}
|
||||
|
||||
.mcb-copy-btn.copied {
|
||||
background: rgba(0, 255, 0, 0.2);
|
||||
border-color: #00ff00;
|
||||
color: #00ff00;
|
||||
}
|
||||
|
||||
/* Code Wrapper */
|
||||
.mcb-code-wrapper {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.mcb-code-wrapper pre {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
.mcb-code-wrapper code {
|
||||
display: block;
|
||||
font-family: inherit;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
/* Line Numbers */
|
||||
.line-numbers .line-number {
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
text-align: right;
|
||||
padding-right: 12px;
|
||||
color: #858585;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.line-numbers .line-content {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
/* Status Bar */
|
||||
.mcb-status-bar {
|
||||
padding: 4px 16px;
|
||||
background: rgba(0, 122, 204, 0.15);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.mcb-status-text {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.mcb-file-info {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Loading Spinner */
|
||||
.mcb-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.mcb-spinner {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
border-top-color: #007acc;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.mcb-loading span {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Error State */
|
||||
.mcb-error {
|
||||
padding: 16px;
|
||||
background: #ff5252;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
margin: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Scrollbars */
|
||||
.mcb-file-list::-webkit-scrollbar,
|
||||
.mcb-code-wrapper::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.mcb-file-list::-webkit-scrollbar-track,
|
||||
.mcb-code-wrapper::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.mcb-file-list::-webkit-scrollbar-thumb,
|
||||
.mcb-code-wrapper::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.mcb-file-list::-webkit-scrollbar-thumb:hover,
|
||||
.mcb-code-wrapper::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* Fullscreen Mode */
|
||||
.maple-code-blocks.fullscreen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 999999;
|
||||
height: 100vh !important;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.mcb-sidebar {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.mcb-code-wrapper {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.line-numbers .line-number {
|
||||
width: 30px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.mcb-sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mcb-header {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.mcb-repo-info {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
187
native/wordpress/maple-code-blocks/assets/css/prism.css
Normal file
187
native/wordpress/maple-code-blocks/assets/css/prism.css
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
/* PrismJS - Minimal syntax highlighting styles */
|
||||
/* Optimized for GitHub Code Viewer Plugin */
|
||||
|
||||
/* Base styles */
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: #d4d4d4;
|
||||
background: none;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: 1.6;
|
||||
tab-size: 4;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
/* Token colors - Dark theme default */
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: #6a9955;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.token.namespace {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag,
|
||||
.token.boolean,
|
||||
.token.number,
|
||||
.token.constant,
|
||||
.token.symbol,
|
||||
.token.deleted {
|
||||
color: #b5cea8;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.builtin,
|
||||
.token.inserted {
|
||||
color: #ce9178;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.language-css .token.string,
|
||||
.style .token.string {
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.keyword {
|
||||
color: #569cd6;
|
||||
}
|
||||
|
||||
.token.function,
|
||||
.token.class-name {
|
||||
color: #dcdcaa;
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important,
|
||||
.token.variable {
|
||||
color: #d16969;
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
/* Light theme overrides */
|
||||
.maple-code-blocks[data-theme="light"] .token.comment,
|
||||
.maple-code-blocks[data-theme="light"] .token.prolog,
|
||||
.maple-code-blocks[data-theme="light"] .token.doctype,
|
||||
.maple-code-blocks[data-theme="light"] .token.cdata {
|
||||
color: #008000;
|
||||
}
|
||||
|
||||
.maple-code-blocks[data-theme="light"] .token.punctuation {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.maple-code-blocks[data-theme="light"] .token.property,
|
||||
.maple-code-blocks[data-theme="light"] .token.tag,
|
||||
.maple-code-blocks[data-theme="light"] .token.boolean,
|
||||
.maple-code-blocks[data-theme="light"] .token.number,
|
||||
.maple-code-blocks[data-theme="light"] .token.constant,
|
||||
.maple-code-blocks[data-theme="light"] .token.symbol {
|
||||
color: #098658;
|
||||
}
|
||||
|
||||
.maple-code-blocks[data-theme="light"] .token.selector,
|
||||
.maple-code-blocks[data-theme="light"] .token.attr-name,
|
||||
.maple-code-blocks[data-theme="light"] .token.string,
|
||||
.maple-code-blocks[data-theme="light"] .token.char,
|
||||
.maple-code-blocks[data-theme="light"] .token.builtin,
|
||||
.maple-code-blocks[data-theme="light"] .token.inserted {
|
||||
color: #a31515;
|
||||
}
|
||||
|
||||
.maple-code-blocks[data-theme="light"] .token.operator,
|
||||
.maple-code-blocks[data-theme="light"] .token.entity,
|
||||
.maple-code-blocks[data-theme="light"] .token.url,
|
||||
.maple-code-blocks[data-theme="light"] .language-css .token.string,
|
||||
.maple-code-blocks[data-theme="light"] .style .token.string {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.maple-code-blocks[data-theme="light"] .token.atrule,
|
||||
.maple-code-blocks[data-theme="light"] .token.attr-value,
|
||||
.maple-code-blocks[data-theme="light"] .token.keyword {
|
||||
color: #0000ff;
|
||||
}
|
||||
|
||||
.maple-code-blocks[data-theme="light"] .token.function {
|
||||
color: #795e26;
|
||||
}
|
||||
|
||||
.maple-code-blocks[data-theme="light"] .token.class-name {
|
||||
color: #267f99;
|
||||
}
|
||||
|
||||
.maple-code-blocks[data-theme="light"] .token.regex,
|
||||
.maple-code-blocks[data-theme="light"] .token.important,
|
||||
.maple-code-blocks[data-theme="light"] .token.variable {
|
||||
color: #e90;
|
||||
}
|
||||
|
||||
/* Line highlighting */
|
||||
.line-highlight {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-top: 1em;
|
||||
background: linear-gradient(to right, rgba(255, 255, 255, .1) 70%, rgba(255, 255, 255, 0));
|
||||
pointer-events: none;
|
||||
line-height: inherit;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
/* Selection styling */
|
||||
pre[class*="language-"]::-moz-selection,
|
||||
pre[class*="language-"] ::-moz-selection,
|
||||
code[class*="language-"]::-moz-selection,
|
||||
code[class*="language-"] ::-moz-selection {
|
||||
text-shadow: none;
|
||||
background: #264f78;
|
||||
}
|
||||
|
||||
pre[class*="language-"]::selection,
|
||||
pre[class*="language-"] ::selection,
|
||||
code[class*="language-"]::selection,
|
||||
code[class*="language-"] ::selection {
|
||||
text-shadow: none;
|
||||
background: #264f78;
|
||||
}
|
||||
|
||||
/* Light theme selection */
|
||||
.maple-code-blocks[data-theme="light"] pre[class*="language-"]::selection,
|
||||
.maple-code-blocks[data-theme="light"] pre[class*="language-"] ::selection,
|
||||
.maple-code-blocks[data-theme="light"] code[class*="language-"]::selection,
|
||||
.maple-code-blocks[data-theme="light"] code[class*="language-"] ::selection {
|
||||
background: #add6ff;
|
||||
}
|
||||
2
native/wordpress/maple-code-blocks/assets/index.php
Normal file
2
native/wordpress/maple-code-blocks/assets/index.php
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?php
|
||||
// Silence is golden.
|
||||
442
native/wordpress/maple-code-blocks/assets/js/block-editor.js
Normal file
442
native/wordpress/maple-code-blocks/assets/js/block-editor.js
Normal file
|
|
@ -0,0 +1,442 @@
|
|||
/**
|
||||
* Maple Code Blocks - Gutenberg Block
|
||||
*
|
||||
* Block registration for the Maple Code Blocks plugin
|
||||
*/
|
||||
|
||||
(function(blocks, element, editor, components, i18n, data, apiFetch) {
|
||||
'use strict';
|
||||
|
||||
const el = element.createElement;
|
||||
const { registerBlockType } = blocks;
|
||||
const { Fragment } = element;
|
||||
const { InspectorControls, BlockControls, AlignmentToolbar, useBlockProps } = editor || {};
|
||||
|
||||
// For older WordPress versions, fallback
|
||||
const BlockProps = useBlockProps || function(props) { return props; };
|
||||
const {
|
||||
PanelBody,
|
||||
PanelRow,
|
||||
TextControl,
|
||||
SelectControl,
|
||||
ToggleControl,
|
||||
Button,
|
||||
Placeholder,
|
||||
Spinner,
|
||||
Notice,
|
||||
ToolbarGroup,
|
||||
ToolbarButton,
|
||||
__experimentalUnitControl: UnitControl,
|
||||
ExternalLink
|
||||
} = components;
|
||||
const { __ } = i18n;
|
||||
const { useState, useEffect } = element;
|
||||
const { useSelect } = data;
|
||||
|
||||
|
||||
// Debug: Log that script is loading
|
||||
console.log('Maple Code Blocks: Script loaded, attempting to register block');
|
||||
|
||||
// Register the block
|
||||
registerBlockType('maple-code-blocks/code-block', {
|
||||
title: __('Maple Code Block', 'maple-code-blocks'),
|
||||
description: __('Display code from GitHub, GitLab, Bitbucket, or Codeberg repositories', 'maple-code-blocks'),
|
||||
icon: {
|
||||
src: 'editor-code',
|
||||
background: '#0366d6',
|
||||
foreground: '#ffffff'
|
||||
},
|
||||
category: 'maple-code-blocks',
|
||||
keywords: ['github', 'code', 'repository', 'syntax', 'highlight', 'viewer'],
|
||||
attributes: {
|
||||
repository: {
|
||||
type: 'string',
|
||||
default: ''
|
||||
},
|
||||
theme: {
|
||||
type: 'string',
|
||||
default: 'dark'
|
||||
},
|
||||
height: {
|
||||
type: 'string',
|
||||
default: '600px'
|
||||
},
|
||||
showLineNumbers: {
|
||||
type: 'boolean',
|
||||
default: true
|
||||
},
|
||||
initialFile: {
|
||||
type: 'string',
|
||||
default: ''
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
default: ''
|
||||
},
|
||||
isValid: {
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
repoFiles: {
|
||||
type: 'array',
|
||||
default: []
|
||||
}
|
||||
},
|
||||
supports: {
|
||||
align: ['wide', 'full'],
|
||||
className: true,
|
||||
customClassName: true,
|
||||
html: false,
|
||||
anchor: true
|
||||
},
|
||||
|
||||
edit: function(props) {
|
||||
const { attributes, setAttributes, className, isSelected } = props;
|
||||
const {
|
||||
repository,
|
||||
theme,
|
||||
height,
|
||||
showLineNumbers,
|
||||
initialFile,
|
||||
title,
|
||||
isValid,
|
||||
repoFiles
|
||||
} = attributes;
|
||||
|
||||
const [isValidating, setIsValidating] = useState(false);
|
||||
const [validationError, setValidationError] = useState('');
|
||||
const [isLoadingFiles, setIsLoadingFiles] = useState(false);
|
||||
const [popularRepo, setPopularRepo] = useState('');
|
||||
|
||||
const blockProps = BlockProps({
|
||||
className: className + ' mcb-block-editor'
|
||||
});
|
||||
|
||||
// Validate repository when it changes
|
||||
useEffect(() => {
|
||||
if (repository && repository.includes('/')) {
|
||||
validateRepository();
|
||||
}
|
||||
}, [repository]);
|
||||
|
||||
// Validate repository format and existence
|
||||
const validateRepository = () => {
|
||||
setIsValidating(true);
|
||||
setValidationError('');
|
||||
|
||||
// Basic format validation
|
||||
const repoPattern = /^[a-zA-Z0-9\-_]+\/[a-zA-Z0-9\-_\.]+$/;
|
||||
if (!repoPattern.test(repository)) {
|
||||
setValidationError('Invalid format. Use: owner/repository');
|
||||
setIsValidating(false);
|
||||
setAttributes({ isValid: false });
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate with API
|
||||
apiFetch({
|
||||
path: '/maple-code-blocks/v1/validate-repo',
|
||||
method: 'POST',
|
||||
data: { repository: repository }
|
||||
}).then(response => {
|
||||
setAttributes({ isValid: true });
|
||||
setValidationError('');
|
||||
loadRepositoryFiles();
|
||||
}).catch(error => {
|
||||
setValidationError(error.message || 'Repository not found or inaccessible');
|
||||
setAttributes({ isValid: false });
|
||||
}).finally(() => {
|
||||
setIsValidating(false);
|
||||
});
|
||||
};
|
||||
|
||||
// Load repository files for initial file selection
|
||||
const loadRepositoryFiles = () => {
|
||||
setIsLoadingFiles(true);
|
||||
|
||||
apiFetch({
|
||||
path: '/maple-code-blocks/v1/get-files',
|
||||
method: 'POST',
|
||||
data: { repository: repository }
|
||||
}).then(response => {
|
||||
setAttributes({ repoFiles: response.files || [] });
|
||||
}).catch(error => {
|
||||
console.error('Failed to load files:', error);
|
||||
}).finally(() => {
|
||||
setIsLoadingFiles(false);
|
||||
});
|
||||
};
|
||||
|
||||
// Set a popular repository
|
||||
const setPopularRepository = (repo) => {
|
||||
setAttributes({ repository: repo });
|
||||
setPopularRepo('');
|
||||
};
|
||||
|
||||
// Height units for the control
|
||||
const units = [
|
||||
{ value: 'px', label: 'px' },
|
||||
{ value: '%', label: '%' },
|
||||
{ value: 'vh', label: 'vh' },
|
||||
{ value: 'em', label: 'em' },
|
||||
{ value: 'rem', label: 'rem' }
|
||||
];
|
||||
|
||||
return el(Fragment, {},
|
||||
// Block Controls Toolbar
|
||||
el(BlockControls, {},
|
||||
el(ToolbarGroup, {},
|
||||
el(ToolbarButton, {
|
||||
icon: 'update',
|
||||
label: __('Refresh Repository', 'maple-code-blocks'),
|
||||
onClick: validateRepository,
|
||||
disabled: !repository || isValidating
|
||||
}),
|
||||
el(ToolbarButton, {
|
||||
icon: 'external',
|
||||
label: __('View on GitHub', 'maple-code-blocks'),
|
||||
onClick: () => window.open('https://github.com/' + repository, '_blank'),
|
||||
disabled: !repository || !isValid
|
||||
})
|
||||
)
|
||||
),
|
||||
|
||||
// Inspector Controls (Sidebar)
|
||||
el(InspectorControls, {},
|
||||
el(PanelBody, {
|
||||
title: __('Repository Settings', 'maple-code-blocks'),
|
||||
initialOpen: true
|
||||
},
|
||||
el(TextControl, {
|
||||
label: __('GitHub Repository', 'maple-code-blocks'),
|
||||
value: repository,
|
||||
onChange: (value) => setAttributes({ repository: value }),
|
||||
placeholder: 'owner/repository',
|
||||
help: __('Format: owner/repository (e.g., facebook/react)', 'maple-code-blocks')
|
||||
}),
|
||||
|
||||
isValidating && el(Spinner),
|
||||
|
||||
validationError && el(Notice, {
|
||||
status: 'error',
|
||||
isDismissible: false
|
||||
}, validationError),
|
||||
|
||||
isValid && !isValidating && el(Notice, {
|
||||
status: 'success',
|
||||
isDismissible: false
|
||||
}, __('✓ Repository validated', 'maple-code-blocks')),
|
||||
|
||||
el(PanelRow, {},
|
||||
el('div', { style: { width: '100%' } },
|
||||
el('label', {}, __('Popular Repositories', 'maple-code-blocks')),
|
||||
el(SelectControl, {
|
||||
value: popularRepo,
|
||||
onChange: setPopularRepository,
|
||||
options: [
|
||||
{ label: __('Select a repository...', 'maple-code-blocks'), value: '' },
|
||||
...mcbBlockData.popularRepos.map(repo => ({
|
||||
label: repo,
|
||||
value: repo
|
||||
}))
|
||||
]
|
||||
})
|
||||
)
|
||||
),
|
||||
|
||||
repository && el(ExternalLink, {
|
||||
href: 'https://github.com/' + repository
|
||||
}, __('View on GitHub →', 'maple-code-blocks'))
|
||||
),
|
||||
|
||||
el(PanelBody, {
|
||||
title: __('Display Settings', 'maple-code-blocks'),
|
||||
initialOpen: false
|
||||
},
|
||||
el(TextControl, {
|
||||
label: __('Title (Optional)', 'maple-code-blocks'),
|
||||
value: title,
|
||||
onChange: (value) => setAttributes({ title: value }),
|
||||
placeholder: __('e.g., React Source Code', 'maple-code-blocks')
|
||||
}),
|
||||
|
||||
el(SelectControl, {
|
||||
label: __('Theme', 'maple-code-blocks'),
|
||||
value: theme,
|
||||
onChange: (value) => setAttributes({ theme: value }),
|
||||
options: mcbBlockData.themes
|
||||
}),
|
||||
|
||||
el('div', {
|
||||
className: 'mcb-height-control',
|
||||
style: { marginBottom: '20px' }
|
||||
},
|
||||
el('label', {}, __('Height', 'maple-code-blocks')),
|
||||
el(TextControl, {
|
||||
value: height,
|
||||
onChange: (value) => setAttributes({ height: value }),
|
||||
placeholder: '600px',
|
||||
help: __('Examples: 600px, 80vh, 100%', 'maple-code-blocks')
|
||||
})
|
||||
),
|
||||
|
||||
el(ToggleControl, {
|
||||
label: __('Show Line Numbers', 'maple-code-blocks'),
|
||||
checked: showLineNumbers,
|
||||
onChange: (value) => setAttributes({ showLineNumbers: value })
|
||||
})
|
||||
),
|
||||
|
||||
el(PanelBody, {
|
||||
title: __('Advanced Settings', 'maple-code-blocks'),
|
||||
initialOpen: false
|
||||
},
|
||||
isLoadingFiles && el(Spinner),
|
||||
|
||||
!isLoadingFiles && repoFiles.length > 0 && el(SelectControl, {
|
||||
label: __('Initial File to Display', 'maple-code-blocks'),
|
||||
value: initialFile,
|
||||
onChange: (value) => setAttributes({ initialFile: value }),
|
||||
options: [
|
||||
{ label: __('None (Show file browser)', 'maple-code-blocks'), value: '' },
|
||||
...repoFiles.map(file => ({
|
||||
label: file.name,
|
||||
value: file.path
|
||||
}))
|
||||
],
|
||||
help: __('Select a file to display when the viewer loads', 'maple-code-blocks')
|
||||
})
|
||||
)
|
||||
),
|
||||
|
||||
// Main Block Content
|
||||
el('div', blockProps,
|
||||
!repository ?
|
||||
// Empty state placeholder
|
||||
el(Placeholder, {
|
||||
icon: 'editor-code',
|
||||
label: __('GitHub Code Viewer', 'maple-code-blocks'),
|
||||
instructions: __('Display code from any public GitHub repository', 'maple-code-blocks')
|
||||
},
|
||||
el(TextControl, {
|
||||
value: repository,
|
||||
onChange: (value) => setAttributes({ repository: value }),
|
||||
placeholder: 'owner/repository',
|
||||
label: __('Repository', 'maple-code-blocks')
|
||||
}),
|
||||
el('div', { style: { marginTop: '10px' } },
|
||||
el('strong', {}, __('Quick Start:', 'maple-code-blocks')),
|
||||
el('div', { style: { marginTop: '5px' } },
|
||||
mcbBlockData.popularRepos.slice(0, 3).map(repo =>
|
||||
el(Button, {
|
||||
key: repo,
|
||||
isSecondary: true,
|
||||
onClick: () => setAttributes({ repository: repo }),
|
||||
style: { margin: '2px' }
|
||||
}, repo)
|
||||
)
|
||||
)
|
||||
)
|
||||
) :
|
||||
// Preview state
|
||||
el('div', {
|
||||
className: 'mcb-block-preview',
|
||||
style: {
|
||||
background: theme === 'dark' ? '#1e1e1e' : '#ffffff',
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: '4px',
|
||||
padding: '20px',
|
||||
minHeight: '200px'
|
||||
}
|
||||
},
|
||||
title && el('h3', {
|
||||
style: {
|
||||
margin: '0 0 10px 0',
|
||||
color: theme === 'dark' ? '#ffffff' : '#000000'
|
||||
}
|
||||
}, title),
|
||||
|
||||
el('div', {
|
||||
className: 'mcb-preview-header',
|
||||
style: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginBottom: '15px',
|
||||
paddingBottom: '10px',
|
||||
borderBottom: '1px solid ' + (theme === 'dark' ? '#444' : '#e0e0e0')
|
||||
}
|
||||
},
|
||||
el('svg', {
|
||||
width: '20',
|
||||
height: '20',
|
||||
viewBox: '0 0 24 24',
|
||||
style: { marginRight: '10px' }
|
||||
},
|
||||
el('path', {
|
||||
fill: theme === 'dark' ? '#ffffff' : '#000000',
|
||||
d: 'M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z'
|
||||
})
|
||||
),
|
||||
el('span', {
|
||||
style: {
|
||||
fontSize: '14px',
|
||||
color: theme === 'dark' ? '#d4d4d4' : '#666'
|
||||
}
|
||||
}, repository)
|
||||
),
|
||||
|
||||
el('div', {
|
||||
className: 'mcb-preview-info',
|
||||
style: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '15px',
|
||||
fontSize: '13px',
|
||||
color: theme === 'dark' ? '#969696' : '#666'
|
||||
}
|
||||
},
|
||||
el('div', {}, '🎨 Theme: ' + theme),
|
||||
el('div', {}, '📏 Height: ' + height),
|
||||
el('div', {}, showLineNumbers ? '✅ Line Numbers' : '❌ No Line Numbers'),
|
||||
initialFile && el('div', {}, '📄 Initial: ' + initialFile.split('/').pop())
|
||||
),
|
||||
|
||||
isValidating && el('div', {
|
||||
style: {
|
||||
textAlign: 'center',
|
||||
padding: '20px',
|
||||
color: theme === 'dark' ? '#ffffff' : '#000000'
|
||||
}
|
||||
}, el(Spinner), ' Validating repository...'),
|
||||
|
||||
!isValidating && isValid && el('div', {
|
||||
style: {
|
||||
marginTop: '15px',
|
||||
padding: '10px',
|
||||
background: theme === 'dark' ? '#0e4429' : '#d4edda',
|
||||
borderRadius: '4px',
|
||||
color: theme === 'dark' ? '#52c41a' : '#155724',
|
||||
fontSize: '13px'
|
||||
}
|
||||
}, '✓ Repository validated and ready to display')
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
save: function(props) {
|
||||
// For server-side rendered blocks, we need to return something
|
||||
// This will be replaced by the PHP render callback
|
||||
return el('div', { className: 'maple-code-block-placeholder' }, 'Loading...');
|
||||
}
|
||||
});
|
||||
|
||||
})(
|
||||
window.wp.blocks,
|
||||
window.wp.element,
|
||||
window.wp.blockEditor || window.wp.editor,
|
||||
window.wp.components,
|
||||
window.wp.i18n,
|
||||
window.wp.data,
|
||||
window.wp.apiFetch
|
||||
);
|
||||
225
native/wordpress/maple-code-blocks/assets/js/block-variations.js
Normal file
225
native/wordpress/maple-code-blocks/assets/js/block-variations.js
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
/**
|
||||
* GitHub Code Viewer Block Variations
|
||||
* Provides pre-configured block patterns for common use cases
|
||||
*/
|
||||
|
||||
(function(blocks, domReady) {
|
||||
'use strict';
|
||||
|
||||
domReady(function() {
|
||||
// Register block variations
|
||||
blocks.registerBlockVariation('maple-code-blocks/code-block', {
|
||||
name: 'react-component',
|
||||
title: 'Maple: React Component',
|
||||
description: 'Display a React component from GitHub',
|
||||
icon: 'editor-code',
|
||||
attributes: {
|
||||
repository: 'facebook/react',
|
||||
theme: 'dark',
|
||||
height: '500px',
|
||||
showLineNumbers: true,
|
||||
initialFile: 'packages/react/src/React.js'
|
||||
},
|
||||
scope: ['inserter']
|
||||
});
|
||||
|
||||
blocks.registerBlockVariation('maple-code-blocks/code-block', {
|
||||
name: 'documentation-viewer',
|
||||
title: 'Maple: Documentation Viewer',
|
||||
description: 'Display README or documentation files',
|
||||
icon: 'media-document',
|
||||
attributes: {
|
||||
repository: '',
|
||||
theme: 'light',
|
||||
height: '400px',
|
||||
showLineNumbers: false,
|
||||
initialFile: 'README.md'
|
||||
},
|
||||
scope: ['inserter']
|
||||
});
|
||||
|
||||
blocks.registerBlockVariation('maple-code-blocks/code-block', {
|
||||
name: 'full-repository',
|
||||
title: 'Maple: Full Repository Browser',
|
||||
description: 'Browse entire repository with file tree',
|
||||
icon: 'category',
|
||||
attributes: {
|
||||
repository: '',
|
||||
theme: 'dark',
|
||||
height: '700px',
|
||||
showLineNumbers: true,
|
||||
initialFile: ''
|
||||
},
|
||||
scope: ['inserter']
|
||||
});
|
||||
|
||||
blocks.registerBlockVariation('maple-code-blocks/code-block', {
|
||||
name: 'code-snippet',
|
||||
title: 'Maple: Code Snippet',
|
||||
description: 'Display a specific code file',
|
||||
icon: 'editor-code',
|
||||
attributes: {
|
||||
repository: '',
|
||||
theme: 'monokai',
|
||||
height: '300px',
|
||||
showLineNumbers: true,
|
||||
initialFile: ''
|
||||
},
|
||||
scope: ['inserter']
|
||||
});
|
||||
|
||||
blocks.registerBlockVariation('maple-code-blocks/code-block', {
|
||||
name: 'tutorial-code',
|
||||
title: 'Maple: Tutorial Code',
|
||||
description: 'Perfect for coding tutorials and education',
|
||||
icon: 'welcome-learn-more',
|
||||
attributes: {
|
||||
repository: '',
|
||||
theme: 'solarized',
|
||||
height: '450px',
|
||||
showLineNumbers: true,
|
||||
title: 'Example Code'
|
||||
},
|
||||
scope: ['inserter']
|
||||
});
|
||||
|
||||
// Register block styles (additional styling options)
|
||||
blocks.registerBlockStyle('maple-code-blocks/code-block', {
|
||||
name: 'default',
|
||||
label: 'Default',
|
||||
isDefault: true
|
||||
});
|
||||
|
||||
blocks.registerBlockStyle('maple-code-blocks/code-block', {
|
||||
name: 'minimal',
|
||||
label: 'Minimal',
|
||||
className: 'is-style-minimal'
|
||||
});
|
||||
|
||||
blocks.registerBlockStyle('maple-code-blocks/code-block', {
|
||||
name: 'rounded',
|
||||
label: 'Rounded',
|
||||
className: 'is-style-rounded'
|
||||
});
|
||||
|
||||
blocks.registerBlockStyle('maple-code-blocks/code-block', {
|
||||
name: 'shadowed',
|
||||
label: 'Shadowed',
|
||||
className: 'is-style-shadowed'
|
||||
});
|
||||
|
||||
// Register block patterns for complete layouts
|
||||
if (wp.blockEditor && wp.blockEditor.registerBlockPattern) {
|
||||
// Code Comparison Pattern
|
||||
wp.blockEditor.registerBlockPattern('maple-code-blocks/code-comparison', {
|
||||
title: 'Maple: Code Comparison',
|
||||
description: 'Compare code from two different repositories',
|
||||
categories: ['maple-code-blocks'],
|
||||
content: `
|
||||
<!-- wp:columns -->
|
||||
<div class="wp-block-columns">
|
||||
<!-- wp:column -->
|
||||
<div class="wp-block-column">
|
||||
<!-- wp:heading {"level":3} -->
|
||||
<h3>Original Implementation</h3>
|
||||
<!-- /wp:heading -->
|
||||
<!-- wp:maple-code-blocks/code-block {"repository":"facebook/react","theme":"dark","height":"400px"} /-->
|
||||
</div>
|
||||
<!-- /wp:column -->
|
||||
<!-- wp:column -->
|
||||
<div class="wp-block-column">
|
||||
<!-- wp:heading {"level":3} -->
|
||||
<h3>Alternative Implementation</h3>
|
||||
<!-- /wp:heading -->
|
||||
<!-- wp:maple-code-blocks/code-block {"repository":"vuejs/vue","theme":"dark","height":"400px"} /-->
|
||||
</div>
|
||||
<!-- /wp:column -->
|
||||
</div>
|
||||
<!-- /wp:columns -->`
|
||||
});
|
||||
|
||||
// Tutorial Pattern
|
||||
wp.blockEditor.registerBlockPattern('maple-code-blocks/tutorial-layout', {
|
||||
title: 'Maple: Tutorial Layout',
|
||||
description: 'Code tutorial with explanation',
|
||||
categories: ['maple-code-blocks'],
|
||||
content: `
|
||||
<!-- wp:group {"backgroundColor":"light-gray","padding":{"top":"40px","right":"40px","bottom":"40px","left":"40px"}} -->
|
||||
<div class="wp-block-group has-light-gray-background-color has-background" style="padding-top:40px;padding-right:40px;padding-bottom:40px;padding-left:40px">
|
||||
<!-- wp:heading -->
|
||||
<h2>Code Tutorial</h2>
|
||||
<!-- /wp:heading -->
|
||||
<!-- wp:paragraph -->
|
||||
<p>Here's an example of how to implement this feature:</p>
|
||||
<!-- /wp:paragraph -->
|
||||
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"light","height":"350px","showLineNumbers":true,"title":"Step 1: Basic Setup"} /-->
|
||||
<!-- wp:paragraph -->
|
||||
<p>Now let's add some advanced functionality:</p>
|
||||
<!-- /wp:paragraph -->
|
||||
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"light","height":"350px","showLineNumbers":true,"title":"Step 2: Advanced Features"} /-->
|
||||
</div>
|
||||
<!-- /wp:group -->`
|
||||
});
|
||||
|
||||
// Documentation Pattern
|
||||
wp.blockEditor.registerBlockPattern('maple-code-blocks/documentation', {
|
||||
title: 'Maple: Documentation Section',
|
||||
description: 'Documentation with embedded code',
|
||||
categories: ['maple-code-blocks'],
|
||||
content: `
|
||||
<!-- wp:heading -->
|
||||
<h2>API Documentation</h2>
|
||||
<!-- /wp:heading -->
|
||||
<!-- wp:paragraph -->
|
||||
<p>This library provides a simple interface for working with the API.</p>
|
||||
<!-- /wp:paragraph -->
|
||||
<!-- wp:heading {"level":3} -->
|
||||
<h3>Installation</h3>
|
||||
<!-- /wp:heading -->
|
||||
<!-- wp:code -->
|
||||
<pre class="wp-block-code"><code>npm install example-library</code></pre>
|
||||
<!-- /wp:code -->
|
||||
<!-- wp:heading {"level":3} -->
|
||||
<h3>Source Code</h3>
|
||||
<!-- /wp:heading -->
|
||||
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"dark","height":"500px","showLineNumbers":true} /-->
|
||||
<!-- wp:heading {"level":3} -->
|
||||
<h3>Examples</h3>
|
||||
<!-- /wp:heading -->
|
||||
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"dark","height":"300px","showLineNumbers":false,"initialFile":"examples/basic.js"} /-->`
|
||||
});
|
||||
|
||||
// Showcase Pattern
|
||||
wp.blockEditor.registerBlockPattern('maple-code-blocks/showcase', {
|
||||
title: 'Maple: Project Showcase',
|
||||
description: 'Showcase a GitHub project',
|
||||
categories: ['maple-code-blocks'],
|
||||
content: `
|
||||
<!-- wp:cover {"url":"","dimRatio":50,"align":"full"} -->
|
||||
<div class="wp-block-cover alignfull">
|
||||
<div class="wp-block-cover__inner-container">
|
||||
<!-- wp:heading {"textAlign":"center","level":1} -->
|
||||
<h1 class="has-text-align-center">Project Name</h1>
|
||||
<!-- /wp:heading -->
|
||||
<!-- wp:paragraph {"align":"center"} -->
|
||||
<p class="has-text-align-center">A brief description of your amazing project</p>
|
||||
<!-- /wp:paragraph -->
|
||||
<!-- wp:buttons {"layout":{"type":"flex","justifyContent":"center"}} -->
|
||||
<div class="wp-block-buttons">
|
||||
<!-- wp:button -->
|
||||
<div class="wp-block-button"><a class="wp-block-button__link">View on GitHub</a></div>
|
||||
<!-- /wp:button -->
|
||||
</div>
|
||||
<!-- /wp:buttons -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- /wp:cover -->
|
||||
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"dark","height":"600px","showLineNumbers":true,"align":"full"} /-->`
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
})(
|
||||
window.wp.blocks,
|
||||
window.wp.domReady
|
||||
);
|
||||
2
native/wordpress/maple-code-blocks/assets/js/index.php
Normal file
2
native/wordpress/maple-code-blocks/assets/js/index.php
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?php
|
||||
// Silence is golden.
|
||||
647
native/wordpress/maple-code-blocks/assets/js/mcb-script.js
Normal file
647
native/wordpress/maple-code-blocks/assets/js/mcb-script.js
Normal file
|
|
@ -0,0 +1,647 @@
|
|||
/**
|
||||
* GitHub Code Viewer - Main JavaScript
|
||||
*/
|
||||
|
||||
(function($) {
|
||||
'use strict';
|
||||
|
||||
// GitHub Code Viewer Class
|
||||
class GitHubCodeViewer {
|
||||
constructor(element) {
|
||||
this.$element = $(element);
|
||||
this.repo = this.$element.data('repo');
|
||||
this.theme = this.$element.data('theme');
|
||||
this.showLineNumbers = this.$element.data('show-line-numbers');
|
||||
this.initialFile = this.$element.data('initial-file');
|
||||
|
||||
this.files = [];
|
||||
this.openTabs = [];
|
||||
this.activeTab = null;
|
||||
this.fileCache = {};
|
||||
this.activeRequests = []; // Track active AJAX requests
|
||||
this.currentPath = ''; // Track current folder path
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.bindEvents();
|
||||
this.loadRepositoryFiles();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// Search functionality with debouncing
|
||||
let searchTimeout;
|
||||
this.$element.on('input', '.mcb-search-input', (e) => {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
this.filterFiles($(e.target).val());
|
||||
}, 300); // Debounce for 300ms
|
||||
});
|
||||
|
||||
// File/folder selection
|
||||
this.$element.on('click', '.mcb-file-item', (e) => {
|
||||
const $item = $(e.currentTarget);
|
||||
const isFolder = $item.data('is-folder');
|
||||
const path = $item.data('path');
|
||||
|
||||
if (isFolder) {
|
||||
// Navigate to folder
|
||||
this.loadRepositoryFiles(path);
|
||||
} else {
|
||||
// Load file
|
||||
this.loadFile(path);
|
||||
}
|
||||
});
|
||||
|
||||
// Tab management
|
||||
this.$element.on('click', '.mcb-tab', (e) => {
|
||||
const $tab = $(e.currentTarget);
|
||||
const filePath = $tab.data('path');
|
||||
this.switchToTab(filePath);
|
||||
});
|
||||
|
||||
this.$element.on('click', '.mcb-tab-close', (e) => {
|
||||
e.stopPropagation();
|
||||
const $tab = $(e.target).closest('.mcb-tab');
|
||||
const filePath = $tab.data('path');
|
||||
this.closeTab(filePath);
|
||||
});
|
||||
|
||||
// Copy button
|
||||
this.$element.on('click', '.mcb-copy-btn', (e) => {
|
||||
this.copyCode($(e.target));
|
||||
});
|
||||
|
||||
// Home button - go to root
|
||||
this.$element.on('click', '.mcb-home-btn', () => {
|
||||
this.loadRepositoryFiles(''); // Load root
|
||||
});
|
||||
|
||||
// Refresh button
|
||||
this.$element.on('click', '.mcb-refresh-btn', () => {
|
||||
this.refreshFiles();
|
||||
});
|
||||
|
||||
// Fullscreen toggle
|
||||
this.$element.on('click', '.mcb-fullscreen-btn', () => {
|
||||
this.toggleFullscreen();
|
||||
});
|
||||
}
|
||||
|
||||
loadRepositoryFiles(path = '') {
|
||||
console.log('MCB: Loading repository files for:', this.repo, 'path:', path);
|
||||
const $fileList = this.$element.find('.mcb-file-list');
|
||||
|
||||
// Update current path
|
||||
this.currentPath = path;
|
||||
|
||||
// Show loading indicator
|
||||
$fileList.html('<div class="mcb-loading"><div class="mcb-spinner"></div><span>Loading files...</span></div>');
|
||||
|
||||
// Abort any pending requests
|
||||
this.abortActiveRequests();
|
||||
|
||||
console.log('MCB: Making AJAX request to:', mcb_ajax.ajax_url);
|
||||
const request = $.ajax({
|
||||
url: mcb_ajax.ajax_url,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'mcb_get_repo_files',
|
||||
repo: this.repo,
|
||||
path: path,
|
||||
nonce: mcb_ajax.nonce
|
||||
},
|
||||
success: (response) => {
|
||||
console.log('MCB: AJAX response:', response);
|
||||
this.removeRequest(request);
|
||||
if (response.success) {
|
||||
console.log('MCB: Files loaded:', response.data);
|
||||
this.files = response.data;
|
||||
this.renderFileList();
|
||||
|
||||
// Load initial file if specified
|
||||
if (this.initialFile) {
|
||||
const file = this.files.find(f => f.path === this.initialFile);
|
||||
if (file) {
|
||||
this.loadFile(this.initialFile);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error('MCB: Error:', response.data);
|
||||
$fileList.html('<div class="mcb-error">' + response.data + '</div>');
|
||||
}
|
||||
},
|
||||
error: (xhr, status, error) => {
|
||||
console.error('MCB: AJAX failed:', status, error);
|
||||
this.removeRequest(request);
|
||||
$fileList.html('<div class="mcb-error">Failed to load repository files: ' + error + '</div>');
|
||||
}
|
||||
});
|
||||
|
||||
this.activeRequests.push(request);
|
||||
}
|
||||
|
||||
renderFileList() {
|
||||
const $fileList = this.$element.find('.mcb-file-list');
|
||||
$fileList.empty();
|
||||
|
||||
// Add current path breadcrumb if not at root
|
||||
if (this.currentPath) {
|
||||
const $breadcrumb = $('<div class="mcb-breadcrumb">');
|
||||
$breadcrumb.append('<span class="mcb-path-label">Path: </span>');
|
||||
$breadcrumb.append('<span class="mcb-current-path">/' + this.escapeHtml(this.currentPath) + '</span>');
|
||||
$fileList.append($breadcrumb);
|
||||
}
|
||||
|
||||
// Render files and folders
|
||||
this.files.forEach(file => {
|
||||
const $item = $('<div class="mcb-file-item">')
|
||||
.attr('data-path', file.path)
|
||||
.attr('data-name', file.name.toLowerCase())
|
||||
.attr('data-is-folder', file.is_folder || false);
|
||||
|
||||
// Add appropriate icon
|
||||
let icon;
|
||||
if (file.type === 'parent') {
|
||||
icon = '⬆'; // Up arrow for parent
|
||||
$item.addClass('mcb-parent-folder');
|
||||
} else if (file.is_folder) {
|
||||
icon = '📁'; // Folder icon
|
||||
$item.addClass('mcb-folder');
|
||||
} else {
|
||||
icon = this.getFileIcon(file.type);
|
||||
$item.addClass('mcb-file');
|
||||
}
|
||||
|
||||
$item.append('<span class="mcb-file-icon">' + icon + '</span>');
|
||||
$item.append('<span class="mcb-file-name">' + this.escapeHtml(file.name) + '</span>');
|
||||
|
||||
// Add file size for files only
|
||||
if (!file.is_folder) {
|
||||
const sizeStr = this.formatFileSize(file.size);
|
||||
$item.append('<span class="mcb-file-size">' + sizeStr + '</span>');
|
||||
}
|
||||
|
||||
$fileList.append($item);
|
||||
});
|
||||
}
|
||||
|
||||
organizeFileTree(files) {
|
||||
const tree = {};
|
||||
files.forEach(file => {
|
||||
const parts = file.path.split('/');
|
||||
let current = tree;
|
||||
|
||||
parts.forEach((part, index) => {
|
||||
if (index === parts.length - 1) {
|
||||
// It's a file
|
||||
current[part] = file;
|
||||
} else {
|
||||
// It's a directory
|
||||
if (!current[part]) {
|
||||
current[part] = {};
|
||||
}
|
||||
current = current[part];
|
||||
}
|
||||
});
|
||||
});
|
||||
return tree;
|
||||
}
|
||||
|
||||
filterFiles(searchTerm) {
|
||||
const $items = this.$element.find('.mcb-file-item');
|
||||
const term = searchTerm.toLowerCase();
|
||||
|
||||
$items.each((index, item) => {
|
||||
const $item = $(item);
|
||||
const fileName = $item.data('name');
|
||||
|
||||
if (!term || fileName.includes(term)) {
|
||||
$item.show();
|
||||
} else {
|
||||
$item.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadFile(filePath) {
|
||||
// Limit cache size to prevent memory issues
|
||||
this.limitCacheSize();
|
||||
|
||||
// Check if file is already in cache
|
||||
if (this.fileCache[filePath]) {
|
||||
this.displayFile(filePath, this.fileCache[filePath]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update status
|
||||
this.updateStatus('Loading file...');
|
||||
|
||||
const request = $.ajax({
|
||||
url: mcb_ajax.ajax_url,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'mcb_load_file',
|
||||
repo: this.repo,
|
||||
file_path: filePath,
|
||||
nonce: mcb_ajax.nonce
|
||||
},
|
||||
success: (response) => {
|
||||
this.removeRequest(request);
|
||||
if (response.success) {
|
||||
this.fileCache[filePath] = response.data;
|
||||
this.displayFile(filePath, response.data);
|
||||
this.updateStatus('Ready');
|
||||
} else {
|
||||
this.showError('Failed to load file: ' + response.data);
|
||||
this.updateStatus('Error loading file');
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
this.removeRequest(request);
|
||||
this.showError('Network error while loading file');
|
||||
this.updateStatus('Network error');
|
||||
}
|
||||
});
|
||||
|
||||
this.activeRequests.push(request);
|
||||
}
|
||||
|
||||
displayFile(filePath, fileData) {
|
||||
// Store in cache
|
||||
this.fileCache[filePath] = fileData;
|
||||
|
||||
// Limit number of open tabs to prevent memory issues
|
||||
const maxTabs = 10;
|
||||
|
||||
// Add to tabs if not already open
|
||||
if (!this.openTabs.includes(filePath)) {
|
||||
// Check tab limit
|
||||
if (this.openTabs.length >= maxTabs) {
|
||||
// Close the oldest tab
|
||||
const oldestTab = this.openTabs[0];
|
||||
this.closeTab(oldestTab);
|
||||
}
|
||||
|
||||
this.openTabs.push(filePath);
|
||||
this.addTab(filePath, fileData.filename);
|
||||
}
|
||||
|
||||
// Only switch to tab if not already active
|
||||
if (this.activeTab !== filePath) {
|
||||
this.switchToTab(filePath);
|
||||
} else {
|
||||
// Just update the content if already active
|
||||
const $codeArea = this.$element.find('.mcb-code-area');
|
||||
const safeContent = this.createSafeCodeDisplay(fileData.content, fileData.filename);
|
||||
$codeArea.html(safeContent);
|
||||
|
||||
// Apply syntax highlighting
|
||||
if (typeof Prism !== 'undefined') {
|
||||
Prism.highlightAll();
|
||||
}
|
||||
|
||||
// Update status
|
||||
const fileSize = this.formatFileSize(fileData.content.length);
|
||||
const lineCount = fileData.content.split('\n').length;
|
||||
this.$element.find('.mcb-file-info').text(fileData.filename + ' • ' + lineCount + ' lines • ' + fileSize);
|
||||
}
|
||||
|
||||
// Display content
|
||||
const $codeArea = this.$element.find('.mcb-code-area');
|
||||
|
||||
// Create safe HTML content
|
||||
const safeContent = this.createSafeCodeDisplay(fileData.content, fileData.filename);
|
||||
$codeArea.html(safeContent);
|
||||
|
||||
// Apply syntax highlighting if Prism is loaded
|
||||
if (typeof Prism !== 'undefined') {
|
||||
Prism.highlightAll();
|
||||
}
|
||||
|
||||
// Mark file as active in sidebar
|
||||
this.$element.find('.mcb-file-item').removeClass('active');
|
||||
this.$element.find('.mcb-file-item[data-path="' + filePath + '"]').addClass('active');
|
||||
|
||||
// Update file info
|
||||
const file = this.files.find(f => f.path === filePath);
|
||||
if (file) {
|
||||
this.updateFileInfo(file);
|
||||
}
|
||||
}
|
||||
|
||||
createSafeCodeDisplay(content, filename) {
|
||||
// The content is already escaped by PHP, but we'll double-check
|
||||
const $container = $('<div class="mcb-code-container">');
|
||||
|
||||
// Header
|
||||
const $header = $('<div class="mcb-code-header">');
|
||||
$header.append($('<span class="mcb-filename">').text(filename));
|
||||
|
||||
// Copy button - store content in data, not attribute
|
||||
const $copyBtn = $('<button class="mcb-copy-btn">Copy</button>');
|
||||
$copyBtn.data('content', content); // Use data() instead of attr()
|
||||
$header.append($copyBtn);
|
||||
|
||||
$container.append($header);
|
||||
|
||||
// Code wrapper
|
||||
const $wrapper = $('<div class="mcb-code-wrapper">');
|
||||
const language = this.detectLanguage(filename);
|
||||
|
||||
// Create pre/code structure
|
||||
const $pre = $('<pre>').addClass('line-numbers');
|
||||
const $code = $('<code>').addClass('language-' + language);
|
||||
|
||||
// CRITICAL: Ensure content is text, not HTML
|
||||
$code.text(content);
|
||||
|
||||
$pre.append($code);
|
||||
$wrapper.append($pre);
|
||||
$container.append($wrapper);
|
||||
|
||||
return $container;
|
||||
}
|
||||
|
||||
addTab(filePath, filename) {
|
||||
const $tabs = this.$element.find('.mcb-tabs');
|
||||
|
||||
const $tab = $('<div class="mcb-tab">')
|
||||
.attr('data-path', filePath);
|
||||
|
||||
$tab.append('<span class="mcb-tab-title">' + this.escapeHtml(filename) + '</span>');
|
||||
$tab.append('<span class="mcb-tab-close">×</span>');
|
||||
|
||||
$tabs.append($tab);
|
||||
}
|
||||
|
||||
switchToTab(filePath) {
|
||||
// Prevent switching if already active
|
||||
if (this.activeTab === filePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.activeTab = filePath;
|
||||
|
||||
// Update tab states
|
||||
this.$element.find('.mcb-tab').removeClass('active');
|
||||
this.$element.find('.mcb-tab[data-path="' + filePath + '"]').addClass('active');
|
||||
|
||||
// Load file content if cached, otherwise just display the tab
|
||||
if (this.fileCache[filePath]) {
|
||||
// Display cached content without calling displayFile
|
||||
const $codeArea = this.$element.find('.mcb-code-area');
|
||||
const fileData = this.fileCache[filePath];
|
||||
const safeContent = this.createSafeCodeDisplay(fileData.content, fileData.filename);
|
||||
$codeArea.html(safeContent);
|
||||
|
||||
// Apply syntax highlighting if Prism is loaded
|
||||
if (typeof Prism !== 'undefined') {
|
||||
Prism.highlightAll();
|
||||
}
|
||||
|
||||
// Update status
|
||||
const fileSize = this.formatFileSize(fileData.content.length);
|
||||
const lineCount = fileData.content.split('\n').length;
|
||||
this.$element.find('.mcb-file-info').text(fileData.filename + ' • ' + lineCount + ' lines • ' + fileSize);
|
||||
}
|
||||
}
|
||||
|
||||
closeTab(filePath) {
|
||||
// Remove from open tabs
|
||||
const index = this.openTabs.indexOf(filePath);
|
||||
if (index > -1) {
|
||||
this.openTabs.splice(index, 1);
|
||||
}
|
||||
|
||||
// Remove tab element
|
||||
this.$element.find('.mcb-tab[data-path="' + filePath + '"]').remove();
|
||||
|
||||
// If this was the active tab, switch to another
|
||||
if (this.activeTab === filePath) {
|
||||
if (this.openTabs.length > 0) {
|
||||
this.switchToTab(this.openTabs[this.openTabs.length - 1]);
|
||||
} else {
|
||||
// Show welcome screen
|
||||
this.showWelcome();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
copyCode($button) {
|
||||
const content = $button.data('content'); // Use data() instead of attr()
|
||||
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create temporary textarea
|
||||
const $temp = $('<textarea>');
|
||||
$temp.val(content);
|
||||
$('body').append($temp);
|
||||
$temp.select();
|
||||
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
$button.text('Copied!').addClass('copied');
|
||||
setTimeout(() => {
|
||||
$button.text('Copy').removeClass('copied');
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
}
|
||||
|
||||
$temp.remove();
|
||||
}
|
||||
|
||||
refreshFiles() {
|
||||
// Abort any pending requests first
|
||||
this.abortActiveRequests();
|
||||
|
||||
// Clear cache
|
||||
this.fileCache = {};
|
||||
|
||||
// Reload files at current path
|
||||
this.loadRepositoryFiles(this.currentPath);
|
||||
this.updateStatus('Repository refreshed');
|
||||
}
|
||||
|
||||
toggleFullscreen() {
|
||||
this.$element.toggleClass('fullscreen');
|
||||
}
|
||||
|
||||
showWelcome() {
|
||||
const $codeArea = this.$element.find('.mcb-code-area');
|
||||
const welcome = `
|
||||
<div class="mcb-welcome">
|
||||
<svg viewBox="0 0 24 24" width="64" height="64">
|
||||
<path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/>
|
||||
</svg>
|
||||
<h4>GitHub Code Viewer</h4>
|
||||
<p>Select a file from the sidebar to view its content</p>
|
||||
</div>
|
||||
`;
|
||||
$codeArea.html(welcome);
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
const $codeArea = this.$element.find('.mcb-code-area');
|
||||
$codeArea.html('<div class="mcb-error">' + this.escapeHtml(message) + '</div>');
|
||||
}
|
||||
|
||||
updateStatus(text) {
|
||||
this.$element.find('.mcb-status-text').text(text);
|
||||
}
|
||||
|
||||
updateFileInfo(file) {
|
||||
const info = file.name + ' • ' + this.formatFileSize(file.size) + ' • ' + file.type;
|
||||
this.$element.find('.mcb-file-info').text(info);
|
||||
}
|
||||
|
||||
formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
detectLanguage(filename) {
|
||||
const ext = filename.split('.').pop().toLowerCase();
|
||||
const languageMap = {
|
||||
'js': 'javascript',
|
||||
'jsx': 'jsx',
|
||||
'ts': 'typescript',
|
||||
'tsx': 'tsx',
|
||||
'py': 'python',
|
||||
'rb': 'ruby',
|
||||
'php': 'php',
|
||||
'java': 'java',
|
||||
'c': 'c',
|
||||
'cpp': 'cpp',
|
||||
'cs': 'csharp',
|
||||
'go': 'go',
|
||||
'rs': 'rust',
|
||||
'swift': 'swift',
|
||||
'kt': 'kotlin',
|
||||
'scala': 'scala',
|
||||
'r': 'r',
|
||||
'sql': 'sql',
|
||||
'sh': 'bash',
|
||||
'yml': 'yaml',
|
||||
'json': 'json',
|
||||
'xml': 'xml',
|
||||
'html': 'html',
|
||||
'css': 'css',
|
||||
'scss': 'scss',
|
||||
'md': 'markdown'
|
||||
};
|
||||
|
||||
return languageMap[ext] || 'plain';
|
||||
}
|
||||
|
||||
getFileIcon(type) {
|
||||
const icons = {
|
||||
'javascript': '📜',
|
||||
'python': '🐍',
|
||||
'php': '🐘',
|
||||
'java': '☕',
|
||||
'cpp': '⚙️',
|
||||
'go': '🐹',
|
||||
'rust': '🦀',
|
||||
'ruby': '💎',
|
||||
'html': '🌐',
|
||||
'css': '🎨',
|
||||
'json': '📋',
|
||||
'markdown': '📝',
|
||||
'default': '📄'
|
||||
};
|
||||
|
||||
return icons[type] || icons.default;
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
const map = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
|
||||
return text.replace(/[&<>"']/g, m => map[m]);
|
||||
}
|
||||
|
||||
limitCacheSize() {
|
||||
// Limit cache to 20 files to prevent memory issues
|
||||
const maxCacheSize = 20;
|
||||
const cacheKeys = Object.keys(this.fileCache);
|
||||
|
||||
if (cacheKeys.length >= maxCacheSize) {
|
||||
// Remove oldest cached files (FIFO)
|
||||
const toRemove = cacheKeys.slice(0, cacheKeys.length - maxCacheSize + 1);
|
||||
toRemove.forEach(key => {
|
||||
delete this.fileCache[key];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
cleanupBeforeUnload() {
|
||||
// Clean up event listeners and large objects
|
||||
this.$element.off();
|
||||
this.abortActiveRequests();
|
||||
this.fileCache = null;
|
||||
this.files = null;
|
||||
this.openTabs = null;
|
||||
}
|
||||
|
||||
abortActiveRequests() {
|
||||
// Abort all pending AJAX requests
|
||||
if (this.activeRequests && this.activeRequests.length > 0) {
|
||||
this.activeRequests.forEach(request => {
|
||||
if (request && request.abort) {
|
||||
request.abort();
|
||||
}
|
||||
});
|
||||
this.activeRequests = [];
|
||||
}
|
||||
}
|
||||
|
||||
removeRequest(request) {
|
||||
const index = this.activeRequests.indexOf(request);
|
||||
if (index > -1) {
|
||||
this.activeRequests.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize all viewers on page
|
||||
$(document).ready(() => {
|
||||
console.log('MCB: Document ready, looking for .maple-code-blocks elements');
|
||||
const viewers = [];
|
||||
|
||||
$('.maple-code-blocks').each(function() {
|
||||
console.log('MCB: Initializing viewer for element:', this);
|
||||
try {
|
||||
const viewer = new GitHubCodeViewer(this);
|
||||
viewers.push(viewer);
|
||||
console.log('MCB: Viewer initialized successfully');
|
||||
} catch (error) {
|
||||
console.error('MCB: Error initializing viewer:', error);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('MCB: Total viewers initialized:', viewers.length);
|
||||
|
||||
// Cleanup on page unload to prevent memory leaks
|
||||
$(window).on('beforeunload', () => {
|
||||
viewers.forEach(viewer => {
|
||||
if (viewer && viewer.cleanupBeforeUnload) {
|
||||
viewer.cleanupBeforeUnload();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
})(jQuery);
|
||||
182
native/wordpress/maple-code-blocks/assets/js/prism.js
Normal file
182
native/wordpress/maple-code-blocks/assets/js/prism.js
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
/* PrismJS - Minimal Core for GitHub Code Viewer */
|
||||
/* This is a simplified version focused on safety and basic highlighting */
|
||||
|
||||
(function(){
|
||||
|
||||
if (typeof self === 'undefined' || !self.Prism || !self.document) {
|
||||
return;
|
||||
}
|
||||
|
||||
var Prism = window.Prism = {
|
||||
languages: {},
|
||||
|
||||
// Simplified tokenization with infinite loop prevention
|
||||
tokenize: function(text, grammar) {
|
||||
// Prevent tokenizing huge files
|
||||
if (text.length > 100000) {
|
||||
return [text]; // Return as plain text for very large files
|
||||
}
|
||||
|
||||
var tokens = [];
|
||||
var rest = text;
|
||||
var maxIterations = 10000; // Prevent infinite loops
|
||||
var iterations = 0;
|
||||
|
||||
while (rest.length > 0 && iterations < maxIterations) {
|
||||
iterations++;
|
||||
var matchFound = false;
|
||||
|
||||
for (var token in grammar) {
|
||||
if (!grammar.hasOwnProperty(token) || !grammar[token]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var pattern = grammar[token];
|
||||
if (pattern instanceof RegExp) {
|
||||
var matches = rest.match(pattern);
|
||||
|
||||
if (matches && matches.index === 0) {
|
||||
matchFound = true;
|
||||
tokens.push({
|
||||
type: token,
|
||||
content: matches[0]
|
||||
});
|
||||
rest = rest.substring(matches[0].length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no match found, consume one character as plain text
|
||||
if (!matchFound) {
|
||||
if (tokens.length > 0 && typeof tokens[tokens.length - 1] === 'string') {
|
||||
tokens[tokens.length - 1] += rest.charAt(0);
|
||||
} else {
|
||||
tokens.push(rest.charAt(0));
|
||||
}
|
||||
rest = rest.substring(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Add any remaining text
|
||||
if (rest.length > 0) {
|
||||
tokens.push(rest);
|
||||
}
|
||||
|
||||
return tokens;
|
||||
},
|
||||
|
||||
// Highlight element
|
||||
highlightElement: function(element) {
|
||||
var language = element.className.match(/language-(\w+)/);
|
||||
if (!language) return;
|
||||
|
||||
language = language[1];
|
||||
var grammar = Prism.languages[language];
|
||||
|
||||
if (!grammar) return;
|
||||
|
||||
// Get text content (already HTML escaped by PHP)
|
||||
var code = element.textContent;
|
||||
|
||||
// Simple tokenization
|
||||
var tokens = Prism.tokenize(code, grammar);
|
||||
|
||||
// Build highlighted HTML (safe because content is already escaped)
|
||||
var highlighted = tokens.map(function(token) {
|
||||
if (typeof token === 'string') {
|
||||
return token;
|
||||
}
|
||||
return '<span class="token ' + token.type + '">' + token.content + '</span>';
|
||||
}).join('');
|
||||
|
||||
element.innerHTML = highlighted;
|
||||
},
|
||||
|
||||
// Highlight all code blocks
|
||||
highlightAll: function() {
|
||||
var elements = document.querySelectorAll('code[class*="language-"], [class*="language-"] code');
|
||||
for (var i = 0, element; element = elements[i++];) {
|
||||
Prism.highlightElement(element);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Define basic grammars for common languages
|
||||
Prism.languages.javascript = {
|
||||
'comment': /\/\/.*|\/\*[\s\S]*?\*\//,
|
||||
'string': /(["'])(?:\\.|(?!\1)[^\\\r\n])*\1/,
|
||||
'keyword': /\b(?:var|let|const|function|return|if|else|for|while|do|switch|case|break|continue|typeof|instanceof|new|this|throw|try|catch|finally|async|await|class|extends|super|import|export|default|yield)\b/,
|
||||
'boolean': /\b(?:true|false)\b/,
|
||||
'number': /\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i,
|
||||
'function': /[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*(?=\s*\()/,
|
||||
'operator': /[+\-*/%=!<>&|^~?:]+/,
|
||||
'punctuation': /[{}[\];(),.:]/
|
||||
};
|
||||
|
||||
Prism.languages.python = {
|
||||
'comment': /#.*/,
|
||||
'string': /(["'])(?:\\.|(?!\1)[^\\\r\n])*\1/,
|
||||
'keyword': /\b(?:and|as|assert|async|await|break|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|not|or|pass|print|raise|return|try|while|with|yield)\b/,
|
||||
'boolean': /\b(?:True|False|None)\b/,
|
||||
'number': /\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?j?/i,
|
||||
'operator': /[+\-*/%=!<>&|^~]+/,
|
||||
'punctuation': /[{}[\];(),.:]/
|
||||
};
|
||||
|
||||
Prism.languages.php = {
|
||||
'comment': /\/\/.*|\/\*[\s\S]*?\*\/|#.*/,
|
||||
'string': /(["'])(?:\\.|(?!\1)[^\\\r\n])*\1/,
|
||||
'keyword': /\b(?:abstract|and|array|as|break|callable|case|catch|class|clone|const|continue|declare|default|die|do|echo|else|elseif|empty|enddeclare|endfor|endforeach|endif|endswitch|endwhile|eval|exit|extends|final|finally|for|foreach|function|global|goto|if|implements|include|include_once|instanceof|insteadof|interface|isset|list|namespace|new|or|print|private|protected|public|require|require_once|return|static|switch|throw|trait|try|unset|use|var|while|xor|yield)\b/i,
|
||||
'boolean': /\b(?:false|true)\b/i,
|
||||
'variable': /\$\w+/,
|
||||
'number': /\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i,
|
||||
'operator': /[+\-*/%=!<>&|^~?:]+/,
|
||||
'punctuation': /[{}[\];(),.:]/
|
||||
};
|
||||
|
||||
Prism.languages.html = Prism.languages.xml = {
|
||||
'comment': /<!--[\s\S]*?-->/,
|
||||
'tag': {
|
||||
pattern: /<\/?[^\s>\/]+(?:\s+[^\s>\/=]+(?:=(?:("|')(?:\\.|(?!\1)[^\\])*\1|[^\s'">=]+))?)*\s*\/?>/i,
|
||||
inside: {
|
||||
'punctuation': /^<\/?|\/?>$/,
|
||||
'attr-name': /[^\s>\/]+/,
|
||||
'attr-value': /=(?:("|')(?:\\.|(?!\1)[^\\])*\1|[^\s'">=]+)/i
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Prism.languages.css = {
|
||||
'comment': /\/\*[\s\S]*?\*\//,
|
||||
'atrule': /@[\w-]+?.*?(?:;|(?=\s*\{))/i,
|
||||
'url': /url\((?:(["'])(?:\\.|(?!\1)[^\\\r\n])*\1|.*?)\)/i,
|
||||
'selector': /[^{}\s][^{}]*(?=\s*\{)/,
|
||||
'string': /(["'])(?:\\.|(?!\1)[^\\\r\n])*\1/,
|
||||
'property': /[-_a-z\xA0-\uFFFF][-\w\xA0-\uFFFF]*(?=\s*:)/i,
|
||||
'important': /!important\b/i,
|
||||
'punctuation': /[(){};:]/
|
||||
};
|
||||
|
||||
Prism.languages.json = {
|
||||
'property': /"(?:\\.|[^\\"\r\n])*"(?=\s*:)/i,
|
||||
'string': /"(?:\\.|[^\\"\r\n])*"(?!\s*:)/,
|
||||
'number': /\b0x[\dA-Fa-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:[Ee][+-]?\d+)?/,
|
||||
'punctuation': /[{}[\]);,]/,
|
||||
'operator': /:/g,
|
||||
'boolean': /\b(?:true|false)\b/i,
|
||||
'null': /\bnull\b/i
|
||||
};
|
||||
|
||||
// Language aliases
|
||||
Prism.languages.js = Prism.languages.javascript;
|
||||
Prism.languages.py = Prism.languages.python;
|
||||
|
||||
// Auto-initialize
|
||||
if (document.readyState !== 'loading') {
|
||||
setTimeout(Prism.highlightAll, 0);
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', Prism.highlightAll);
|
||||
}
|
||||
|
||||
})();
|
||||
118
native/wordpress/maple-code-blocks/assets/js/simple-block.js
Normal file
118
native/wordpress/maple-code-blocks/assets/js/simple-block.js
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
// Maple Code Blocks - Simple Block Registration
|
||||
console.log('Maple Code Blocks: Starting block registration');
|
||||
|
||||
(function(wp) {
|
||||
// Check if wp.blocks exists
|
||||
if (!wp || !wp.blocks) {
|
||||
console.error('Maple Code Blocks: wp.blocks not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const { registerBlockType } = wp.blocks;
|
||||
const { TextControl, PanelBody, SelectControl } = wp.components;
|
||||
const { InspectorControls } = wp.blockEditor || wp.editor || {};
|
||||
const { Fragment, createElement: el } = wp.element;
|
||||
|
||||
console.log('Maple Code Blocks: Dependencies loaded, registering block...');
|
||||
|
||||
// Register the main Maple Code Block
|
||||
const blockRegistered = registerBlockType('maple-code-blocks/code-block', {
|
||||
title: 'Maple Code Block',
|
||||
description: 'Display code from GitHub, GitLab, Bitbucket or Codeberg',
|
||||
category: 'widgets',
|
||||
icon: 'editor-code',
|
||||
keywords: ['maple', 'code', 'github', 'gitlab', 'bitbucket'],
|
||||
attributes: {
|
||||
repository: {
|
||||
type: 'string',
|
||||
default: ''
|
||||
},
|
||||
theme: {
|
||||
type: 'string',
|
||||
default: 'dark'
|
||||
},
|
||||
height: {
|
||||
type: 'string',
|
||||
default: '600px'
|
||||
}
|
||||
},
|
||||
|
||||
edit: function(props) {
|
||||
const { attributes, setAttributes } = props;
|
||||
const { repository, theme, height } = attributes;
|
||||
|
||||
return el(Fragment, {},
|
||||
el(InspectorControls, {},
|
||||
el(PanelBody, { title: 'Repository Settings', initialOpen: true },
|
||||
el(TextControl, {
|
||||
label: 'Repository',
|
||||
value: repository,
|
||||
onChange: function(value) { setAttributes({ repository: value }) },
|
||||
placeholder: 'e.g., facebook/react or gitlab:gnome/gimp',
|
||||
help: 'Format: [platform:]owner/repo. Platforms: github (default), gitlab, bitbucket, codeberg. Examples: facebook/react, gitlab:gitlab-org/gitlab, bitbucket:atlassian/python-bitbucket, codeberg:forgejo/forgejo'
|
||||
}),
|
||||
el(SelectControl, {
|
||||
label: 'Theme',
|
||||
value: theme,
|
||||
options: [
|
||||
{ label: 'Dark', value: 'dark' },
|
||||
{ label: 'Light', value: 'light' },
|
||||
{ label: 'Monokai', value: 'monokai' },
|
||||
{ label: 'Solarized', value: 'solarized' }
|
||||
],
|
||||
onChange: function(value) { setAttributes({ theme: value }) }
|
||||
}),
|
||||
el(TextControl, {
|
||||
label: 'Height',
|
||||
value: height,
|
||||
onChange: function(value) { setAttributes({ height: value }) },
|
||||
placeholder: '600px'
|
||||
})
|
||||
)
|
||||
),
|
||||
el('div', {
|
||||
className: 'maple-code-block-editor',
|
||||
style: {
|
||||
padding: '20px',
|
||||
backgroundColor: theme === 'dark' ? '#1e1e1e' : '#fff',
|
||||
color: theme === 'dark' ? '#fff' : '#000',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px'
|
||||
}
|
||||
},
|
||||
el('h3', { style: { marginTop: 0 } }, 'Maple Code Block'),
|
||||
repository ?
|
||||
el('p', {}, 'Repository: ', el('strong', {}, repository)) :
|
||||
el('p', { style: { color: '#999' } }, 'Enter a repository in the block settings'),
|
||||
el('p', {}, 'Theme: ', el('strong', {}, theme)),
|
||||
el('p', {}, 'Height: ', el('strong', {}, height))
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
save: function() {
|
||||
// Rendered server-side
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
if (blockRegistered) {
|
||||
console.log('Maple Code Blocks: Block registered successfully!', blockRegistered);
|
||||
} else {
|
||||
console.error('Maple Code Blocks: Block registration failed');
|
||||
}
|
||||
|
||||
})(window.wp);
|
||||
|
||||
// Also try registering with global wp if window.wp fails
|
||||
if (typeof wp !== 'undefined' && wp.blocks && !wp.blocks.getBlockType('maple-code-blocks/code-block')) {
|
||||
console.log('Maple Code Blocks: Attempting registration with global wp');
|
||||
wp.blocks.registerBlockType('maple-code-blocks/code-block', {
|
||||
title: 'Maple Code Block',
|
||||
description: 'Display code from repositories',
|
||||
category: 'widgets',
|
||||
icon: 'editor-code',
|
||||
edit: function() { return wp.element.createElement('div', {}, 'Maple Code Block'); },
|
||||
save: function() { return null; }
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
(function(wp) {
|
||||
const { registerBlockVariation } = wp.blocks;
|
||||
|
||||
wp.domReady(function() {
|
||||
// Register variations for the Maple Code Block
|
||||
registerBlockVariation('maple-code-blocks/code-block', {
|
||||
name: 'maple-github',
|
||||
title: 'Maple: GitHub Code',
|
||||
description: 'Display code from GitHub',
|
||||
icon: 'editor-code',
|
||||
attributes: {
|
||||
repository: 'facebook/react',
|
||||
theme: 'dark'
|
||||
},
|
||||
scope: ['inserter']
|
||||
});
|
||||
|
||||
registerBlockVariation('maple-code-blocks/code-block', {
|
||||
name: 'maple-gitlab',
|
||||
title: 'Maple: GitLab Code',
|
||||
description: 'Display code from GitLab',
|
||||
icon: 'editor-code',
|
||||
attributes: {
|
||||
repository: 'gitlab:gitlab-org/gitlab',
|
||||
theme: 'dark'
|
||||
},
|
||||
scope: ['inserter']
|
||||
});
|
||||
|
||||
registerBlockVariation('maple-code-blocks/code-block', {
|
||||
name: 'maple-documentation',
|
||||
title: 'Maple: Documentation',
|
||||
description: 'Display README or docs',
|
||||
icon: 'media-document',
|
||||
attributes: {
|
||||
repository: '',
|
||||
theme: 'light',
|
||||
height: '400px'
|
||||
},
|
||||
scope: ['inserter']
|
||||
});
|
||||
|
||||
console.log('Maple Code Blocks: Variations registered');
|
||||
});
|
||||
})(window.wp);
|
||||
33
native/wordpress/maple-code-blocks/includes/basic-block.php
Normal file
33
native/wordpress/maple-code-blocks/includes/basic-block.php
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
/**
|
||||
* Ultra Simple Block Registration - PHP Only
|
||||
* This ensures the block is registered even if JavaScript fails
|
||||
*/
|
||||
|
||||
add_action('init', function() {
|
||||
if (!function_exists('register_block_type')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Register a basic block with PHP only
|
||||
register_block_type('maple-code-blocks/basic', array(
|
||||
'title' => 'Maple Code Block (Basic)',
|
||||
'description' => 'Display code from repositories',
|
||||
'category' => 'widgets',
|
||||
'icon' => 'editor-code',
|
||||
'keywords' => array('maple', 'code', 'github'),
|
||||
'attributes' => array(
|
||||
'repository' => array(
|
||||
'type' => 'string',
|
||||
'default' => ''
|
||||
)
|
||||
),
|
||||
'render_callback' => function($attributes) {
|
||||
$repo = isset($attributes['repository']) ? $attributes['repository'] : '';
|
||||
if (empty($repo)) {
|
||||
return '<p>Enter a repository in block settings</p>';
|
||||
}
|
||||
return do_shortcode('[maple_code_block repo="' . esc_attr($repo) . '"]');
|
||||
}
|
||||
));
|
||||
}, 100); // Very late priority to ensure everything is loaded
|
||||
|
|
@ -0,0 +1,351 @@
|
|||
<?php
|
||||
/**
|
||||
* Gutenberg Block Registration
|
||||
* Registers the GitHub Code Viewer block for the block editor
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('WPINC')) {
|
||||
die('Direct access not permitted.');
|
||||
}
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class MCB_Block_Editor {
|
||||
|
||||
/**
|
||||
* Initialize block editor support
|
||||
*/
|
||||
public static function init() {
|
||||
// Check if Gutenberg is available
|
||||
if (!function_exists('register_block_type')) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_action('init', array(__CLASS__, 'register_block'));
|
||||
add_action('enqueue_block_editor_assets', array(__CLASS__, 'enqueue_block_editor_assets'));
|
||||
add_action('enqueue_block_assets', array(__CLASS__, 'enqueue_block_assets'));
|
||||
add_filter('block_categories_all', array(__CLASS__, 'add_block_category'), 10, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the block
|
||||
*/
|
||||
public static function register_block() {
|
||||
// Debug: Log that we're attempting to register
|
||||
error_log('MCB: Attempting to register block');
|
||||
|
||||
// Register block editor script
|
||||
wp_register_script(
|
||||
'mcb-block-editor',
|
||||
MCB_PLUGIN_URL . 'assets/js/block-editor.js',
|
||||
array('wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n', 'wp-data'),
|
||||
MCB_PLUGIN_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
// Register block editor styles
|
||||
wp_register_style(
|
||||
'mcb-block-editor-style',
|
||||
MCB_PLUGIN_URL . 'assets/css/block-editor.css',
|
||||
array('wp-edit-blocks'),
|
||||
MCB_PLUGIN_VERSION
|
||||
);
|
||||
|
||||
// Register the block with simplified attributes first
|
||||
$result = register_block_type('maple-code-blocks/code-block', array(
|
||||
'editor_script' => 'mcb-block-editor',
|
||||
'editor_style' => 'mcb-block-editor-style',
|
||||
'render_callback' => array(__CLASS__, 'render_block'),
|
||||
'attributes' => array(
|
||||
'repository' => array(
|
||||
'type' => 'string',
|
||||
'default' => ''
|
||||
),
|
||||
'theme' => array(
|
||||
'type' => 'string',
|
||||
'default' => 'dark'
|
||||
),
|
||||
'height' => array(
|
||||
'type' => 'string',
|
||||
'default' => '600px'
|
||||
),
|
||||
'showLineNumbers' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true
|
||||
),
|
||||
'initialFile' => array(
|
||||
'type' => 'string',
|
||||
'default' => ''
|
||||
),
|
||||
'title' => array(
|
||||
'type' => 'string',
|
||||
'default' => ''
|
||||
),
|
||||
'align' => array(
|
||||
'type' => 'string',
|
||||
'default' => 'none'
|
||||
),
|
||||
'className' => array(
|
||||
'type' => 'string',
|
||||
'default' => ''
|
||||
)
|
||||
),
|
||||
'supports' => array(
|
||||
'align' => array('wide', 'full'),
|
||||
'className' => true,
|
||||
'customClassName' => true,
|
||||
'html' => false,
|
||||
'anchor' => true
|
||||
),
|
||||
'example' => array(
|
||||
'attributes' => array(
|
||||
'repository' => 'facebook/react',
|
||||
'theme' => 'dark',
|
||||
'height' => '400px',
|
||||
'title' => 'React Source Code Example'
|
||||
)
|
||||
)
|
||||
));
|
||||
|
||||
// Add block variations
|
||||
wp_register_script(
|
||||
'mcb-block-variations',
|
||||
MCB_PLUGIN_URL . 'assets/js/block-variations.js',
|
||||
array('wp-blocks', 'wp-dom-ready'),
|
||||
MCB_PLUGIN_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
wp_enqueue_script('mcb-block-variations');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the block on the frontend
|
||||
*/
|
||||
public static function render_block($attributes, $content) {
|
||||
// Validate required attributes
|
||||
if (empty($attributes['repository'])) {
|
||||
return '<div class="mcb-error">Please specify a repository (e.g., owner/repository)</div>';
|
||||
}
|
||||
|
||||
// Sanitize attributes
|
||||
$repository = sanitize_text_field($attributes['repository']);
|
||||
|
||||
// Validate repository format
|
||||
if (!MCB_Security::validate_repo_format($repository)) {
|
||||
return '<div class="mcb-error">Invalid repository format. Use: owner/repository</div>';
|
||||
}
|
||||
|
||||
// Build shortcode attributes
|
||||
$shortcode_atts = array(
|
||||
'repo' => $repository,
|
||||
'theme' => sanitize_text_field($attributes['theme'] ?? 'dark'),
|
||||
'height' => sanitize_text_field($attributes['height'] ?? '600px'),
|
||||
'show_line_numbers' => $attributes['showLineNumbers'] ? 'true' : 'false',
|
||||
'initial_file' => sanitize_text_field($attributes['initialFile'] ?? ''),
|
||||
'title' => sanitize_text_field($attributes['title'] ?? '')
|
||||
);
|
||||
|
||||
// Add alignment class if needed
|
||||
$wrapper_class = 'mcb-block-wrapper';
|
||||
if (!empty($attributes['align'])) {
|
||||
$wrapper_class .= ' align' . esc_attr($attributes['align']);
|
||||
}
|
||||
if (!empty($attributes['className'])) {
|
||||
$wrapper_class .= ' ' . esc_attr($attributes['className']);
|
||||
}
|
||||
|
||||
// Generate shortcode
|
||||
$shortcode = '[maple_code_block';
|
||||
foreach ($shortcode_atts as $key => $value) {
|
||||
if (!empty($value)) {
|
||||
$shortcode .= ' ' . $key . '="' . esc_attr($value) . '"';
|
||||
}
|
||||
}
|
||||
$shortcode .= ']';
|
||||
|
||||
// Render with wrapper
|
||||
return sprintf(
|
||||
'<div class="%s">%s</div>',
|
||||
esc_attr($wrapper_class),
|
||||
do_shortcode($shortcode)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue block editor assets
|
||||
*/
|
||||
public static function enqueue_block_editor_assets() {
|
||||
// Debug log
|
||||
error_log('MCB: Enqueueing block editor assets');
|
||||
|
||||
// Make sure our script is registered
|
||||
if (!wp_script_is('mcb-block-editor', 'registered')) {
|
||||
error_log('MCB: Script not registered, registering now');
|
||||
wp_register_script(
|
||||
'mcb-block-editor',
|
||||
MCB_PLUGIN_URL . 'assets/js/block-editor.js',
|
||||
array('wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n', 'wp-data'),
|
||||
MCB_PLUGIN_VERSION,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
// Localize script with data for the editor
|
||||
wp_localize_script('mcb-block-editor', 'mcbBlockData', array(
|
||||
'pluginUrl' => MCB_PLUGIN_URL,
|
||||
'themes' => array(
|
||||
array('label' => 'Dark', 'value' => 'dark'),
|
||||
array('label' => 'Light', 'value' => 'light'),
|
||||
array('label' => 'Monokai', 'value' => 'monokai'),
|
||||
array('label' => 'Solarized', 'value' => 'solarized')
|
||||
),
|
||||
'defaultHeight' => '600px',
|
||||
'popularRepos' => array(
|
||||
'facebook/react',
|
||||
'vuejs/vue',
|
||||
'angular/angular',
|
||||
'microsoft/vscode',
|
||||
'torvalds/linux',
|
||||
'tensorflow/tensorflow',
|
||||
'kubernetes/kubernetes',
|
||||
'nodejs/node',
|
||||
'rust-lang/rust',
|
||||
'golang/go'
|
||||
),
|
||||
'nonce' => wp_create_nonce('mcb_block_nonce')
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue frontend block assets
|
||||
*/
|
||||
public static function enqueue_block_assets() {
|
||||
// Only enqueue on frontend
|
||||
if (!is_admin()) {
|
||||
// These will be enqueued by the shortcode handler
|
||||
// We just need to ensure the block wrapper styles are loaded
|
||||
wp_enqueue_style(
|
||||
'mcb-block-style',
|
||||
MCB_PLUGIN_URL . 'assets/css/block-style.css',
|
||||
array(),
|
||||
MCB_PLUGIN_VERSION
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom block category
|
||||
*/
|
||||
public static function add_block_category($categories, $post) {
|
||||
return array_merge(
|
||||
array(
|
||||
array(
|
||||
'slug' => 'maple-code-blocks',
|
||||
'title' => __('Maple Code Blocks', 'maple-code-blocks'),
|
||||
'icon' => 'editor-code'
|
||||
)
|
||||
),
|
||||
$categories
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register REST API endpoint for repository validation
|
||||
*/
|
||||
public static function register_rest_routes() {
|
||||
register_rest_route('maple-code-blocks/v1', '/validate-repo', array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array(__CLASS__, 'validate_repository'),
|
||||
'permission_callback' => function() {
|
||||
return current_user_can('edit_posts');
|
||||
},
|
||||
'args' => array(
|
||||
'repository' => array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_text_field'
|
||||
)
|
||||
)
|
||||
));
|
||||
|
||||
register_rest_route('maple-code-blocks/v1', '/get-files', array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array(__CLASS__, 'get_repository_files'),
|
||||
'permission_callback' => function() {
|
||||
return current_user_can('edit_posts');
|
||||
},
|
||||
'args' => array(
|
||||
'repository' => array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_text_field'
|
||||
)
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate repository via REST API
|
||||
*/
|
||||
public static function validate_repository($request) {
|
||||
$repository = $request->get_param('repository');
|
||||
|
||||
// Validate format
|
||||
if (!MCB_Security::validate_repo_format($repository)) {
|
||||
return new WP_Error('invalid_format', 'Invalid repository format. Use: owner/repository');
|
||||
}
|
||||
|
||||
// Quick check with GitHub API
|
||||
$github_api = new MCB_GitHub_API();
|
||||
$result = $github_api->validate_repository_exists($repository);
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
return array(
|
||||
'valid' => true,
|
||||
'repository' => $repository
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get repository files for block editor preview
|
||||
*/
|
||||
public static function get_repository_files($request) {
|
||||
$repository = $request->get_param('repository');
|
||||
|
||||
// Validate format
|
||||
if (!MCB_Security::validate_repo_format($repository)) {
|
||||
return new WP_Error('invalid_format', 'Invalid repository format');
|
||||
}
|
||||
|
||||
$github_api = new MCB_GitHub_API();
|
||||
$files = $github_api->get_repository_files($repository);
|
||||
|
||||
if (is_wp_error($files)) {
|
||||
return $files;
|
||||
}
|
||||
|
||||
// Return only file names for the editor
|
||||
$file_list = array_map(function($file) {
|
||||
return array(
|
||||
'path' => $file['path'],
|
||||
'name' => $file['name']
|
||||
);
|
||||
}, array_slice($files, 0, 10)); // Limit to 10 files for preview
|
||||
|
||||
return array(
|
||||
'files' => $file_list,
|
||||
'total' => count($files)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize block editor support
|
||||
add_action('init', array('MCB_Block_Editor', 'init'));
|
||||
add_action('rest_api_init', array('MCB_Block_Editor', 'register_rest_routes'));
|
||||
|
|
@ -0,0 +1,273 @@
|
|||
<?php
|
||||
/**
|
||||
* Block Patterns Registration
|
||||
* Registers reusable block patterns for the GitHub Code Viewer
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('WPINC')) {
|
||||
die('Direct access not permitted.');
|
||||
}
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class MCB_Block_Patterns {
|
||||
|
||||
/**
|
||||
* Initialize block patterns
|
||||
*/
|
||||
public static function init() {
|
||||
add_action('init', array(__CLASS__, 'register_pattern_category'));
|
||||
add_action('init', array(__CLASS__, 'register_patterns'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register pattern category
|
||||
*/
|
||||
public static function register_pattern_category() {
|
||||
if (function_exists('register_block_pattern_category')) {
|
||||
register_block_pattern_category(
|
||||
'maple-code-blocks',
|
||||
array(
|
||||
'label' => __('GitHub Code Viewer', 'maple-code-blocks')
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register block patterns
|
||||
*/
|
||||
public static function register_patterns() {
|
||||
if (!function_exists('register_block_pattern')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Code with Explanation Pattern
|
||||
register_block_pattern(
|
||||
'maple-code-blocks/code-with-explanation',
|
||||
array(
|
||||
'title' => __('Maple: Code with Explanation', 'maple-code-blocks'),
|
||||
'description' => __('Display code with explanatory text', 'maple-code-blocks'),
|
||||
'categories' => array('maple-code-blocks', 'text'),
|
||||
'keywords' => array('code', 'github', 'explanation', 'tutorial'),
|
||||
'content' => '<!-- wp:group {"backgroundColor":"white","style":{"spacing":{"padding":{"top":"30px","right":"30px","bottom":"30px","left":"30px"}}}} -->
|
||||
<div class="wp-block-group has-white-background-color has-background" style="padding-top:30px;padding-right:30px;padding-bottom:30px;padding-left:30px">
|
||||
<!-- wp:heading {"level":3} -->
|
||||
<h3>' . __('Understanding the Code', 'maple-code-blocks') . '</h3>
|
||||
<!-- /wp:heading -->
|
||||
|
||||
<!-- wp:paragraph -->
|
||||
<p>' . __('This example demonstrates a key concept in modern development:', 'maple-code-blocks') . '</p>
|
||||
<!-- /wp:paragraph -->
|
||||
|
||||
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"dark","height":"400px","showLineNumbers":true} /-->
|
||||
|
||||
<!-- wp:list -->
|
||||
<ul>
|
||||
<li>' . __('Line 1-5: Initial setup and configuration', 'maple-code-blocks') . '</li>
|
||||
<li>' . __('Line 6-15: Core functionality implementation', 'maple-code-blocks') . '</li>
|
||||
<li>' . __('Line 16-20: Error handling and cleanup', 'maple-code-blocks') . '</li>
|
||||
</ul>
|
||||
<!-- /wp:list -->
|
||||
</div>
|
||||
<!-- /wp:group -->'
|
||||
)
|
||||
);
|
||||
|
||||
// Side-by-side Code Comparison
|
||||
register_block_pattern(
|
||||
'maple-code-blocks/side-by-side',
|
||||
array(
|
||||
'title' => __('Maple: Side-by-Side Comparison', 'maple-code-blocks'),
|
||||
'description' => __('Compare two code implementations', 'maple-code-blocks'),
|
||||
'categories' => array('maple-code-blocks', 'columns'),
|
||||
'keywords' => array('compare', 'code', 'github', 'columns'),
|
||||
'content' => '<!-- wp:columns {"align":"wide"} -->
|
||||
<div class="wp-block-columns alignwide">
|
||||
<!-- wp:column -->
|
||||
<div class="wp-block-column">
|
||||
<!-- wp:heading {"level":4,"style":{"color":{"text":"#0073aa"}}} -->
|
||||
<h4 class="has-text-color" style="color:#0073aa">' . __('Method A', 'maple-code-blocks') . '</h4>
|
||||
<!-- /wp:heading -->
|
||||
|
||||
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"light","height":"350px","showLineNumbers":true} /-->
|
||||
</div>
|
||||
<!-- /wp:column -->
|
||||
|
||||
<!-- wp:column -->
|
||||
<div class="wp-block-column">
|
||||
<!-- wp:heading {"level":4,"style":{"color":{"text":"#00a32a"}}} -->
|
||||
<h4 class="has-text-color" style="color:#00a32a">' . __('Method B', 'maple-code-blocks') . '</h4>
|
||||
<!-- /wp:heading -->
|
||||
|
||||
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"light","height":"350px","showLineNumbers":true} /-->
|
||||
</div>
|
||||
<!-- /wp:column -->
|
||||
</div>
|
||||
<!-- /wp:columns -->'
|
||||
)
|
||||
);
|
||||
|
||||
// Code Gallery Pattern
|
||||
register_block_pattern(
|
||||
'maple-code-blocks/code-gallery',
|
||||
array(
|
||||
'title' => __('Maple: Code Gallery', 'maple-code-blocks'),
|
||||
'description' => __('Showcase multiple code examples', 'maple-code-blocks'),
|
||||
'categories' => array('maple-code-blocks', 'gallery'),
|
||||
'keywords' => array('gallery', 'showcase', 'multiple', 'code'),
|
||||
'content' => '<!-- wp:heading {"textAlign":"center"} -->
|
||||
<h2 class="has-text-align-center">' . __('Code Examples', 'maple-code-blocks') . '</h2>
|
||||
<!-- /wp:heading -->
|
||||
|
||||
<!-- wp:spacer {"height":"30px"} -->
|
||||
<div style="height:30px" aria-hidden="true" class="wp-block-spacer"></div>
|
||||
<!-- /wp:spacer -->
|
||||
|
||||
<!-- wp:columns -->
|
||||
<div class="wp-block-columns">
|
||||
<!-- wp:column -->
|
||||
<div class="wp-block-column">
|
||||
<!-- wp:heading {"level":5,"textAlign":"center"} -->
|
||||
<h5 class="has-text-align-center">' . __('Example 1', 'maple-code-blocks') . '</h5>
|
||||
<!-- /wp:heading -->
|
||||
|
||||
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"monokai","height":"250px","showLineNumbers":false} /-->
|
||||
</div>
|
||||
<!-- /wp:column -->
|
||||
|
||||
<!-- wp:column -->
|
||||
<div class="wp-block-column">
|
||||
<!-- wp:heading {"level":5,"textAlign":"center"} -->
|
||||
<h5 class="has-text-align-center">' . __('Example 2', 'maple-code-blocks') . '</h5>
|
||||
<!-- /wp:heading -->
|
||||
|
||||
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"monokai","height":"250px","showLineNumbers":false} /-->
|
||||
</div>
|
||||
<!-- /wp:column -->
|
||||
|
||||
<!-- wp:column -->
|
||||
<div class="wp-block-column">
|
||||
<!-- wp:heading {"level":5,"textAlign":"center"} -->
|
||||
<h5 class="has-text-align-center">' . __('Example 3', 'maple-code-blocks') . '</h5>
|
||||
<!-- /wp:heading -->
|
||||
|
||||
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"monokai","height":"250px","showLineNumbers":false} /-->
|
||||
</div>
|
||||
<!-- /wp:column -->
|
||||
</div>
|
||||
<!-- /wp:columns -->'
|
||||
)
|
||||
);
|
||||
|
||||
// Featured Code Pattern
|
||||
register_block_pattern(
|
||||
'maple-code-blocks/featured-code',
|
||||
array(
|
||||
'title' => __('Maple: Featured Code', 'maple-code-blocks'),
|
||||
'description' => __('Highlight important code with context', 'maple-code-blocks'),
|
||||
'categories' => array('maple-code-blocks', 'featured'),
|
||||
'keywords' => array('featured', 'highlight', 'code', 'important'),
|
||||
'content' => '<!-- wp:group {"align":"wide","style":{"spacing":{"padding":{"top":"40px","right":"40px","bottom":"40px","left":"40px"}},"border":{"radius":"8px"}},"backgroundColor":"light-gray"} -->
|
||||
<div class="wp-block-group alignwide has-light-gray-background-color has-background" style="border-radius:8px;padding-top:40px;padding-right:40px;padding-bottom:40px;padding-left:40px">
|
||||
<!-- wp:columns -->
|
||||
<div class="wp-block-columns">
|
||||
<!-- wp:column {"width":"40%"} -->
|
||||
<div class="wp-block-column" style="flex-basis:40%">
|
||||
<!-- wp:heading {"level":3} -->
|
||||
<h3>' . __('Featured Implementation', 'maple-code-blocks') . '</h3>
|
||||
<!-- /wp:heading -->
|
||||
|
||||
<!-- wp:paragraph -->
|
||||
<p>' . __('This code demonstrates best practices for this common pattern.', 'maple-code-blocks') . '</p>
|
||||
<!-- /wp:paragraph -->
|
||||
|
||||
<!-- wp:buttons -->
|
||||
<div class="wp-block-buttons">
|
||||
<!-- wp:button -->
|
||||
<div class="wp-block-button"><a class="wp-block-button__link">' . __('View on GitHub', 'maple-code-blocks') . '</a></div>
|
||||
<!-- /wp:button -->
|
||||
</div>
|
||||
<!-- /wp:buttons -->
|
||||
</div>
|
||||
<!-- /wp:column -->
|
||||
|
||||
<!-- wp:column {"width":"60%"} -->
|
||||
<div class="wp-block-column" style="flex-basis:60%">
|
||||
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"dark","height":"350px","showLineNumbers":true} /-->
|
||||
</div>
|
||||
<!-- /wp:column -->
|
||||
</div>
|
||||
<!-- /wp:columns -->
|
||||
</div>
|
||||
<!-- /wp:group -->'
|
||||
)
|
||||
);
|
||||
|
||||
// Tutorial Step Pattern
|
||||
register_block_pattern(
|
||||
'maple-code-blocks/tutorial-steps',
|
||||
array(
|
||||
'title' => __('Maple: Tutorial Steps', 'maple-code-blocks'),
|
||||
'description' => __('Step-by-step code tutorial', 'maple-code-blocks'),
|
||||
'categories' => array('maple-code-blocks', 'text'),
|
||||
'keywords' => array('tutorial', 'steps', 'education', 'learn'),
|
||||
'content' => '<!-- wp:group -->
|
||||
<div class="wp-block-group">
|
||||
<!-- wp:heading -->
|
||||
<h2>' . __('Building Your First Application', 'maple-code-blocks') . '</h2>
|
||||
<!-- /wp:heading -->
|
||||
|
||||
<!-- wp:group {"style":{"spacing":{"margin":{"top":"30px","bottom":"30px"}}}} -->
|
||||
<div class="wp-block-group" style="margin-top:30px;margin-bottom:30px">
|
||||
<!-- wp:heading {"level":3,"style":{"typography":{"fontSize":"20px"}}} -->
|
||||
<h3 style="font-size:20px">📝 ' . __('Step 1: Setup', 'maple-code-blocks') . '</h3>
|
||||
<!-- /wp:heading -->
|
||||
|
||||
<!-- wp:paragraph -->
|
||||
<p>' . __('First, we need to set up our development environment:', 'maple-code-blocks') . '</p>
|
||||
<!-- /wp:paragraph -->
|
||||
|
||||
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"light","height":"200px","showLineNumbers":true} /-->
|
||||
</div>
|
||||
<!-- /wp:group -->
|
||||
|
||||
<!-- wp:group {"style":{"spacing":{"margin":{"top":"30px","bottom":"30px"}}}} -->
|
||||
<div class="wp-block-group" style="margin-top:30px;margin-bottom:30px">
|
||||
<!-- wp:heading {"level":3,"style":{"typography":{"fontSize":"20px"}}} -->
|
||||
<h3 style="font-size:20px">🔧 ' . __('Step 2: Configuration', 'maple-code-blocks') . '</h3>
|
||||
<!-- /wp:heading -->
|
||||
|
||||
<!-- wp:paragraph -->
|
||||
<p>' . __('Next, configure the application settings:', 'maple-code-blocks') . '</p>
|
||||
<!-- /wp:paragraph -->
|
||||
|
||||
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"light","height":"250px","showLineNumbers":true} /-->
|
||||
</div>
|
||||
<!-- /wp:group -->
|
||||
|
||||
<!-- wp:group {"style":{"spacing":{"margin":{"top":"30px","bottom":"30px"}}}} -->
|
||||
<div class="wp-block-group" style="margin-top:30px;margin-bottom:30px">
|
||||
<!-- wp:heading {"level":3,"style":{"typography":{"fontSize":"20px"}}} -->
|
||||
<h3 style="font-size:20px">🚀 ' . __('Step 3: Deploy', 'maple-code-blocks') . '</h3>
|
||||
<!-- /wp:heading -->
|
||||
|
||||
<!-- wp:paragraph -->
|
||||
<p>' . __('Finally, deploy your application:', 'maple-code-blocks') . '</p>
|
||||
<!-- /wp:paragraph -->
|
||||
|
||||
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"light","height":"200px","showLineNumbers":true} /-->
|
||||
</div>
|
||||
<!-- /wp:group -->
|
||||
</div>
|
||||
<!-- /wp:group -->'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize patterns
|
||||
MCB_Block_Patterns::init();
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
<?php
|
||||
/**
|
||||
* Code Renderer Class
|
||||
* Safely renders code content with proper escaping and syntax highlighting
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('WPINC')) {
|
||||
die('Direct access not permitted.');
|
||||
}
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class MCB_Code_Renderer {
|
||||
|
||||
/**
|
||||
* Render code content safely
|
||||
*/
|
||||
public function render_code($content, $filename) {
|
||||
// Validate content before processing
|
||||
$validation = $this->validate_content($content);
|
||||
if (is_wp_error($validation)) {
|
||||
return '<div class="gcv-error">' . esc_html($validation->get_error_message()) . '</div>';
|
||||
}
|
||||
|
||||
// First, ensure the content is treated as plain text
|
||||
// Multiple layers of safety to prevent any code execution
|
||||
|
||||
// 1. Convert to UTF-8 if needed
|
||||
if (!mb_check_encoding($content, 'UTF-8')) {
|
||||
$content = mb_convert_encoding($content, 'UTF-8', mb_detect_encoding($content));
|
||||
}
|
||||
|
||||
// 2. Remove any null bytes
|
||||
$content = str_replace("\0", '', $content);
|
||||
|
||||
// 3. HTML encode everything - this is crucial for safety
|
||||
$safe_content = htmlspecialchars($content, ENT_QUOTES | ENT_HTML5 | ENT_SUBSTITUTE, 'UTF-8', false);
|
||||
|
||||
// 4. Additional escaping for JavaScript context
|
||||
$safe_content = $this->escape_for_javascript($safe_content);
|
||||
|
||||
// 5. Get language for syntax highlighting
|
||||
$language = $this->detect_language($filename);
|
||||
|
||||
// 6. Prepare the code block with line numbers
|
||||
$lines = explode("\n", $safe_content);
|
||||
$formatted_code = $this->format_with_line_numbers($lines);
|
||||
|
||||
// 7. Wrap in proper HTML structure
|
||||
$output = '<div class="gcv-code-container" data-language="' . esc_attr($language) . '">';
|
||||
$output .= '<div class="gcv-code-header">';
|
||||
$output .= '<span class="gcv-filename">' . esc_html(basename($filename)) . '</span>';
|
||||
$output .= '<button class="gcv-copy-btn" data-content="' . esc_attr($safe_content) . '">Copy</button>';
|
||||
$output .= '</div>';
|
||||
$output .= '<div class="gcv-code-wrapper">';
|
||||
$output .= '<pre class="line-numbers"><code class="language-' . esc_attr($language) . '">';
|
||||
$output .= $formatted_code;
|
||||
$output .= '</code></pre>';
|
||||
$output .= '</div>';
|
||||
$output .= '</div>';
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format code with line numbers
|
||||
*/
|
||||
private function format_with_line_numbers($lines) {
|
||||
$output = '';
|
||||
$line_count = count($lines);
|
||||
$digit_count = strlen((string)$line_count);
|
||||
|
||||
foreach ($lines as $index => $line) {
|
||||
$line_num = $index + 1;
|
||||
$padded_num = str_pad($line_num, $digit_count, ' ', STR_PAD_LEFT);
|
||||
$output .= '<span class="line-number" data-line="' . $line_num . '">' . $padded_num . '</span>';
|
||||
$output .= '<span class="line-content">' . $line . '</span>' . "\n";
|
||||
}
|
||||
|
||||
return rtrim($output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional escaping for JavaScript context
|
||||
*/
|
||||
private function escape_for_javascript($content) {
|
||||
// Escape any remaining potentially dangerous patterns
|
||||
$patterns = array(
|
||||
'/<script/i' => '<script',
|
||||
'/<\/script/i' => '</script',
|
||||
'/javascript:/i' => 'javascript:',
|
||||
'/on\w+\s*=/i' => 'on_event=',
|
||||
'/<iframe/i' => '<iframe',
|
||||
'/<object/i' => '<object',
|
||||
'/<embed/i' => '<embed',
|
||||
'/<applet/i' => '<applet'
|
||||
);
|
||||
|
||||
foreach ($patterns as $pattern => $replacement) {
|
||||
$content = preg_replace($pattern, $replacement, $content);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect programming language from filename
|
||||
*/
|
||||
private function detect_language($filename) {
|
||||
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||
$name_lower = strtolower($filename);
|
||||
|
||||
// Map extensions to Prism.js language identifiers
|
||||
$language_map = array(
|
||||
'php' => 'php',
|
||||
'js' => 'javascript',
|
||||
'jsx' => 'jsx',
|
||||
'ts' => 'typescript',
|
||||
'tsx' => 'tsx',
|
||||
'py' => 'python',
|
||||
'rb' => 'ruby',
|
||||
'java' => 'java',
|
||||
'c' => 'c',
|
||||
'cpp' => 'cpp',
|
||||
'cc' => 'cpp',
|
||||
'cxx' => 'cpp',
|
||||
'h' => 'c',
|
||||
'hpp' => 'cpp',
|
||||
'cs' => 'csharp',
|
||||
'swift' => 'swift',
|
||||
'kt' => 'kotlin',
|
||||
'go' => 'go',
|
||||
'rs' => 'rust',
|
||||
'scala' => 'scala',
|
||||
'r' => 'r',
|
||||
'sql' => 'sql',
|
||||
'sh' => 'bash',
|
||||
'bash' => 'bash',
|
||||
'yml' => 'yaml',
|
||||
'yaml' => 'yaml',
|
||||
'json' => 'json',
|
||||
'xml' => 'xml',
|
||||
'html' => 'html',
|
||||
'htm' => 'html',
|
||||
'css' => 'css',
|
||||
'scss' => 'scss',
|
||||
'sass' => 'sass',
|
||||
'less' => 'less',
|
||||
'md' => 'markdown',
|
||||
'markdown' => 'markdown',
|
||||
'txt' => 'plain',
|
||||
'ini' => 'ini',
|
||||
'conf' => 'ini',
|
||||
'cfg' => 'ini'
|
||||
);
|
||||
|
||||
// Special file names
|
||||
if ($name_lower === 'dockerfile') {
|
||||
return 'docker';
|
||||
}
|
||||
if ($name_lower === 'makefile' || $name_lower === 'gnumakefile') {
|
||||
return 'makefile';
|
||||
}
|
||||
if ($name_lower === '.gitignore') {
|
||||
return 'git';
|
||||
}
|
||||
if ($name_lower === '.htaccess') {
|
||||
return 'apacheconf';
|
||||
}
|
||||
if ($name_lower === '.env') {
|
||||
return 'bash';
|
||||
}
|
||||
|
||||
return isset($language_map[$extension]) ? $language_map[$extension] : 'plain';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize and validate file content before rendering
|
||||
*/
|
||||
public function validate_content($content) {
|
||||
// Check for binary content
|
||||
if ($this->is_binary($content)) {
|
||||
return new WP_Error('binary_file', 'Binary files cannot be displayed');
|
||||
}
|
||||
|
||||
// Check file size (limit to 1MB for performance)
|
||||
if (strlen($content) > 1048576) {
|
||||
return new WP_Error('file_too_large', 'File is too large to display (max 1MB)');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if content appears to be binary
|
||||
*/
|
||||
private function is_binary($content) {
|
||||
// Check for null bytes or excessive non-printable characters
|
||||
$null_count = substr_count($content, "\0");
|
||||
if ($null_count > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Sample first 8192 bytes
|
||||
$sample = substr($content, 0, 8192);
|
||||
$non_printable = 0;
|
||||
|
||||
for ($i = 0; $i < strlen($sample); $i++) {
|
||||
$char = ord($sample[$i]);
|
||||
// Allow common whitespace and printable ASCII
|
||||
if ($char < 32 && $char !== 9 && $char !== 10 && $char !== 13) {
|
||||
$non_printable++;
|
||||
}
|
||||
}
|
||||
|
||||
// If more than 30% non-printable, consider it binary
|
||||
return ($non_printable / strlen($sample)) > 0.3;
|
||||
}
|
||||
}
|
||||
749
native/wordpress/maple-code-blocks/includes/class-github-api.php
Normal file
749
native/wordpress/maple-code-blocks/includes/class-github-api.php
Normal file
|
|
@ -0,0 +1,749 @@
|
|||
<?php
|
||||
/**
|
||||
* Git Platform API Handler Class
|
||||
* Handles API interactions for GitHub, GitLab, Bitbucket, and Codeberg
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('WPINC')) {
|
||||
die('Direct access not permitted.');
|
||||
}
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class MCB_GitHub_API {
|
||||
|
||||
private $cache_duration = 3600; // 1 hour cache
|
||||
|
||||
// Platform API configurations
|
||||
private $platforms = array(
|
||||
'github' => array(
|
||||
'name' => 'GitHub',
|
||||
'api_base' => 'https://api.github.com',
|
||||
'repo_pattern' => '/^[a-zA-Z0-9\-_]+\/[a-zA-Z0-9\-_\.]+$/',
|
||||
'web_base' => 'https://github.com'
|
||||
),
|
||||
'gitlab' => array(
|
||||
'name' => 'GitLab',
|
||||
'api_base' => 'https://gitlab.com/api/v4',
|
||||
'repo_pattern' => '/^[a-zA-Z0-9\-_]+\/[a-zA-Z0-9\-_\.]+$/',
|
||||
'web_base' => 'https://gitlab.com'
|
||||
),
|
||||
'bitbucket' => array(
|
||||
'name' => 'Bitbucket',
|
||||
'api_base' => 'https://api.bitbucket.org/2.0',
|
||||
'repo_pattern' => '/^[a-zA-Z0-9\-_]+\/[a-zA-Z0-9\-_\.]+$/',
|
||||
'web_base' => 'https://bitbucket.org'
|
||||
),
|
||||
'codeberg' => array(
|
||||
'name' => 'Codeberg',
|
||||
'api_base' => 'https://codeberg.org/api/v1',
|
||||
'repo_pattern' => '/^[a-zA-Z0-9\-_]+\/[a-zA-Z0-9\-_\.]+$/',
|
||||
'web_base' => 'https://codeberg.org'
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Detect platform from repository string
|
||||
*/
|
||||
private function detect_platform($repo_string) {
|
||||
// Check for platform prefix (e.g., "gitlab:owner/repo")
|
||||
if (strpos($repo_string, ':') !== false) {
|
||||
list($platform, $repo) = explode(':', $repo_string, 2);
|
||||
if (isset($this->platforms[$platform])) {
|
||||
return array('platform' => $platform, 'repo' => $repo);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for full URLs
|
||||
if (strpos($repo_string, 'https://') === 0 || strpos($repo_string, 'http://') === 0) {
|
||||
foreach ($this->platforms as $key => $config) {
|
||||
if (strpos($repo_string, $config['web_base']) !== false) {
|
||||
// Extract owner/repo from URL
|
||||
$parsed = parse_url($repo_string);
|
||||
$path = trim($parsed['path'], '/');
|
||||
$parts = explode('/', $path);
|
||||
if (count($parts) >= 2) {
|
||||
return array(
|
||||
'platform' => $key,
|
||||
'repo' => $parts[0] . '/' . $parts[1]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default to GitHub for backward compatibility
|
||||
return array('platform' => 'github', 'repo' => $repo_string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get repository files list
|
||||
*/
|
||||
public function get_repository_files($repo_string, $path = '') {
|
||||
$platform_info = $this->detect_platform($repo_string);
|
||||
$platform = $platform_info['platform'];
|
||||
$repo = $platform_info['repo'];
|
||||
|
||||
// Validate repo format
|
||||
if (!preg_match($this->platforms[$platform]['repo_pattern'], $repo)) {
|
||||
return new WP_Error('invalid_repo', 'Invalid repository format. Use: owner/repository');
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
$cache_key = 'mcb_repo_' . $platform . '_' . md5($repo . '_' . $path);
|
||||
$cached_data = get_transient($cache_key);
|
||||
|
||||
if ($cached_data !== false) {
|
||||
return $cached_data;
|
||||
}
|
||||
|
||||
// Get files based on platform
|
||||
switch ($platform) {
|
||||
case 'github':
|
||||
$files = $this->get_github_files($repo, $path);
|
||||
break;
|
||||
case 'gitlab':
|
||||
$files = $this->get_gitlab_files($repo, $path);
|
||||
break;
|
||||
case 'bitbucket':
|
||||
$files = $this->get_bitbucket_files($repo, $path);
|
||||
break;
|
||||
case 'codeberg':
|
||||
$files = $this->get_codeberg_files($repo, $path);
|
||||
break;
|
||||
default:
|
||||
return new WP_Error('unsupported_platform', 'Unsupported platform');
|
||||
}
|
||||
|
||||
if (!is_wp_error($files)) {
|
||||
// Cache the results
|
||||
set_transient($cache_key, $files, $this->cache_duration);
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file content from repository
|
||||
*/
|
||||
public function get_file_content($repo_string, $file_path) {
|
||||
$platform_info = $this->detect_platform($repo_string);
|
||||
$platform = $platform_info['platform'];
|
||||
$repo = $platform_info['repo'];
|
||||
|
||||
// Validate inputs
|
||||
if (!preg_match($this->platforms[$platform]['repo_pattern'], $repo)) {
|
||||
return new WP_Error('invalid_repo', 'Invalid repository format');
|
||||
}
|
||||
|
||||
// Sanitize and validate file path
|
||||
$file_path = trim($file_path, '/');
|
||||
|
||||
// Block path traversal attempts
|
||||
if (strpos($file_path, '..') !== false ||
|
||||
strpos($file_path, '//') !== false ||
|
||||
strpos($file_path, '\\') !== false ||
|
||||
preg_match('/[<>"|*?]/', $file_path)) {
|
||||
return new WP_Error('invalid_path', 'Invalid file path');
|
||||
}
|
||||
|
||||
// Check cache
|
||||
$cache_key = 'mcb_file_' . $platform . '_' . md5($repo . $file_path);
|
||||
$cached_content = get_transient($cache_key);
|
||||
|
||||
if ($cached_content !== false) {
|
||||
return $cached_content;
|
||||
}
|
||||
|
||||
// Get content based on platform
|
||||
switch ($platform) {
|
||||
case 'github':
|
||||
$content = $this->get_github_file_content($repo, $file_path);
|
||||
break;
|
||||
case 'gitlab':
|
||||
$content = $this->get_gitlab_file_content($repo, $file_path);
|
||||
break;
|
||||
case 'bitbucket':
|
||||
$content = $this->get_bitbucket_file_content($repo, $file_path);
|
||||
break;
|
||||
case 'codeberg':
|
||||
$content = $this->get_codeberg_file_content($repo, $file_path);
|
||||
break;
|
||||
default:
|
||||
return new WP_Error('unsupported_platform', 'Unsupported platform');
|
||||
}
|
||||
|
||||
if (!is_wp_error($content)) {
|
||||
// Cache the content
|
||||
set_transient($cache_key, $content, $this->cache_duration);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* GitHub-specific file listing
|
||||
*/
|
||||
private function get_github_files($repo, $path = '') {
|
||||
$url = $this->platforms['github']['api_base'] . '/repos/' . $repo . '/contents';
|
||||
if (!empty($path)) {
|
||||
$url .= '/' . ltrim($path, '/');
|
||||
}
|
||||
|
||||
$response = $this->make_api_request($url, 'github');
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
return $this->parse_github_contents($response, $repo, $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* GitLab-specific file listing
|
||||
*/
|
||||
private function get_gitlab_files($repo, $path = '') {
|
||||
// GitLab uses project ID or URL-encoded path
|
||||
$project_id = urlencode($repo);
|
||||
$url = $this->platforms['gitlab']['api_base'] . '/projects/' . $project_id . '/repository/tree?recursive=true&per_page=100';
|
||||
$response = $this->make_api_request($url, 'gitlab');
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
return $this->parse_gitlab_contents($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bitbucket-specific file listing
|
||||
*/
|
||||
private function get_bitbucket_files($repo, $path = '') {
|
||||
$url = $this->platforms['bitbucket']['api_base'] . '/repositories/' . $repo . '/src';
|
||||
$response = $this->make_api_request($url, 'bitbucket');
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
return $this->parse_bitbucket_contents($response, $repo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Codeberg-specific file listing (uses Gitea API)
|
||||
*/
|
||||
private function get_codeberg_files($repo, $path = '') {
|
||||
$url = $this->platforms['codeberg']['api_base'] . '/repos/' . $repo . '/contents';
|
||||
if (!empty($path)) {
|
||||
$url .= '/' . ltrim($path, '/');
|
||||
}
|
||||
|
||||
$response = $this->make_api_request($url, 'codeberg');
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
// Codeberg uses Gitea, similar to GitHub API
|
||||
return $this->parse_github_contents($response, $repo, $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* GitHub file content retrieval
|
||||
*/
|
||||
private function get_github_file_content($repo, $file_path) {
|
||||
$url = $this->platforms['github']['api_base'] . '/repos/' . $repo . '/contents/' . $file_path;
|
||||
$response = $this->make_api_request($url, 'github');
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$data = json_decode($response, true);
|
||||
|
||||
if (!isset($data['content'])) {
|
||||
return new WP_Error('no_content', 'File content not found');
|
||||
}
|
||||
|
||||
return base64_decode($data['content']);
|
||||
}
|
||||
|
||||
/**
|
||||
* GitLab file content retrieval
|
||||
*/
|
||||
private function get_gitlab_file_content($repo, $file_path) {
|
||||
$project_id = urlencode($repo);
|
||||
$file_path_encoded = urlencode($file_path);
|
||||
$url = $this->platforms['gitlab']['api_base'] . '/projects/' . $project_id . '/repository/files/' . $file_path_encoded . '/raw?ref=main';
|
||||
|
||||
// Try main branch first, then master
|
||||
$response = $this->make_api_request($url, 'gitlab');
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
// Try master branch
|
||||
$url = $this->platforms['gitlab']['api_base'] . '/projects/' . $project_id . '/repository/files/' . $file_path_encoded . '/raw?ref=master';
|
||||
$response = $this->make_api_request($url, 'gitlab');
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bitbucket file content retrieval
|
||||
*/
|
||||
private function get_bitbucket_file_content($repo, $file_path) {
|
||||
// Get default branch first
|
||||
$repo_url = $this->platforms['bitbucket']['api_base'] . '/repositories/' . $repo;
|
||||
$repo_response = $this->make_api_request($repo_url, 'bitbucket');
|
||||
|
||||
if (is_wp_error($repo_response)) {
|
||||
return $repo_response;
|
||||
}
|
||||
|
||||
$repo_data = json_decode($repo_response, true);
|
||||
$branch = isset($repo_data['mainbranch']['name']) ? $repo_data['mainbranch']['name'] : 'master';
|
||||
|
||||
$url = $this->platforms['bitbucket']['api_base'] . '/repositories/' . $repo . '/src/' . $branch . '/' . $file_path;
|
||||
return $this->make_api_request($url, 'bitbucket');
|
||||
}
|
||||
|
||||
/**
|
||||
* Codeberg file content retrieval
|
||||
*/
|
||||
private function get_codeberg_file_content($repo, $file_path) {
|
||||
// Codeberg uses Gitea API, similar to GitHub
|
||||
$url = $this->platforms['codeberg']['api_base'] . '/repos/' . $repo . '/contents/' . $file_path;
|
||||
$response = $this->make_api_request($url, 'codeberg');
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$data = json_decode($response, true);
|
||||
|
||||
if (!isset($data['content'])) {
|
||||
return new WP_Error('no_content', 'File content not found');
|
||||
}
|
||||
|
||||
return base64_decode($data['content']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make HTTP request to Git platform API
|
||||
*/
|
||||
private function make_api_request($url, $platform) {
|
||||
// SSRF Protection - validate URL
|
||||
$parsed_url = parse_url($url);
|
||||
|
||||
// Only allow HTTPS protocol
|
||||
if ($parsed_url['scheme'] !== 'https') {
|
||||
return new WP_Error('invalid_protocol', 'Only HTTPS is allowed');
|
||||
}
|
||||
|
||||
// Only allow known platform hosts
|
||||
$allowed_hosts = array(
|
||||
'api.github.com',
|
||||
'gitlab.com',
|
||||
'api.bitbucket.org',
|
||||
'codeberg.org'
|
||||
);
|
||||
|
||||
if (!in_array($parsed_url['host'], $allowed_hosts)) {
|
||||
return new WP_Error('invalid_host', 'Invalid API host');
|
||||
}
|
||||
|
||||
// Add request throttling to prevent rate limit issues
|
||||
static $last_request_time = 0;
|
||||
$min_interval = 0.1; // Minimum 100ms between requests
|
||||
|
||||
$current_time = microtime(true);
|
||||
$time_since_last = $current_time - $last_request_time;
|
||||
|
||||
if ($time_since_last < $min_interval) {
|
||||
usleep(($min_interval - $time_since_last) * 1000000);
|
||||
}
|
||||
|
||||
$args = array(
|
||||
'timeout' => 10, // Reduced timeout to prevent hanging
|
||||
'redirection' => 3, // Limit redirects
|
||||
'headers' => $this->get_platform_headers($platform)
|
||||
);
|
||||
|
||||
// Add authentication if available
|
||||
$token = $this->get_platform_token($platform);
|
||||
if (!empty($token)) {
|
||||
$args['headers']['Authorization'] = $this->get_auth_header($platform, $token);
|
||||
}
|
||||
|
||||
$response = wp_remote_get($url, $args);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$status_code = wp_remote_retrieve_response_code($response);
|
||||
|
||||
if ($status_code !== 200) {
|
||||
return new WP_Error('api_error', 'API returned status: ' . $status_code);
|
||||
}
|
||||
|
||||
$last_request_time = microtime(true);
|
||||
|
||||
return wp_remote_retrieve_body($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get platform-specific headers
|
||||
*/
|
||||
private function get_platform_headers($platform) {
|
||||
switch ($platform) {
|
||||
case 'github':
|
||||
return array(
|
||||
'Accept' => 'application/vnd.github.v3+json',
|
||||
'User-Agent' => 'WordPress/GitHub-Code-Viewer'
|
||||
);
|
||||
case 'gitlab':
|
||||
return array(
|
||||
'Accept' => 'application/json',
|
||||
'User-Agent' => 'WordPress/GitHub-Code-Viewer'
|
||||
);
|
||||
case 'bitbucket':
|
||||
return array(
|
||||
'Accept' => 'application/json',
|
||||
'User-Agent' => 'WordPress/GitHub-Code-Viewer'
|
||||
);
|
||||
case 'codeberg':
|
||||
return array(
|
||||
'Accept' => 'application/json',
|
||||
'User-Agent' => 'WordPress/GitHub-Code-Viewer'
|
||||
);
|
||||
default:
|
||||
return array(
|
||||
'User-Agent' => 'WordPress/GitHub-Code-Viewer'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get platform token if configured
|
||||
*/
|
||||
private function get_platform_token($platform) {
|
||||
// Use secure token manager if available
|
||||
if (class_exists('MCB_Token_Manager')) {
|
||||
return MCB_Token_Manager::get_token($platform);
|
||||
}
|
||||
// Fallback to direct option (for backwards compatibility)
|
||||
return get_option('mcb_' . $platform . '_token', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authorization header format for platform
|
||||
*/
|
||||
private function get_auth_header($platform, $token) {
|
||||
switch ($platform) {
|
||||
case 'github':
|
||||
return 'token ' . $token;
|
||||
case 'gitlab':
|
||||
return 'Bearer ' . $token;
|
||||
case 'bitbucket':
|
||||
return 'Bearer ' . $token;
|
||||
case 'codeberg':
|
||||
return 'token ' . $token;
|
||||
default:
|
||||
return 'Bearer ' . $token;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse GitHub/Codeberg repository contents
|
||||
*/
|
||||
private function parse_github_contents($response, $repo, $current_path = '') {
|
||||
$items = json_decode($response, true);
|
||||
$files = array();
|
||||
|
||||
if (!is_array($items)) {
|
||||
return $files;
|
||||
}
|
||||
|
||||
// Add parent directory navigation if not at root
|
||||
if (!empty($current_path)) {
|
||||
$parent_path = dirname($current_path);
|
||||
if ($parent_path === '.') {
|
||||
$parent_path = '';
|
||||
}
|
||||
|
||||
$files[] = array(
|
||||
'name' => '..',
|
||||
'path' => $parent_path,
|
||||
'size' => 0,
|
||||
'type' => 'parent',
|
||||
'is_folder' => true,
|
||||
'url' => ''
|
||||
);
|
||||
}
|
||||
|
||||
$code_extensions = $this->get_code_extensions();
|
||||
|
||||
// First add all directories
|
||||
foreach ($items as $item) {
|
||||
if ($item['type'] === 'dir') {
|
||||
$files[] = array(
|
||||
'name' => $item['name'],
|
||||
'path' => $item['path'],
|
||||
'size' => 0,
|
||||
'type' => 'folder',
|
||||
'is_folder' => true,
|
||||
'url' => $item['html_url']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Then add all files
|
||||
foreach ($items as $item) {
|
||||
if ($item['type'] === 'file') {
|
||||
$extension = strtolower(pathinfo($item['name'], PATHINFO_EXTENSION));
|
||||
$name_lower = strtolower($item['name']);
|
||||
|
||||
// Include all files, not just code files, for better browsing
|
||||
$files[] = array(
|
||||
'name' => $item['name'],
|
||||
'path' => $item['path'],
|
||||
'size' => $item['size'],
|
||||
'type' => $this->get_file_type($item['name']),
|
||||
'is_folder' => false,
|
||||
'url' => $item['html_url']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse GitLab repository contents
|
||||
*/
|
||||
private function parse_gitlab_contents($response) {
|
||||
$items = json_decode($response, true);
|
||||
$files = array();
|
||||
|
||||
if (!is_array($items)) {
|
||||
return $files;
|
||||
}
|
||||
|
||||
$code_extensions = $this->get_code_extensions();
|
||||
|
||||
foreach ($items as $item) {
|
||||
if ($item['type'] === 'blob') { // GitLab uses 'blob' for files
|
||||
$extension = strtolower(pathinfo($item['name'], PATHINFO_EXTENSION));
|
||||
$name_lower = strtolower($item['name']);
|
||||
|
||||
if (in_array($extension, $code_extensions) || $this->is_code_file($name_lower)) {
|
||||
$files[] = array(
|
||||
'name' => $item['name'],
|
||||
'path' => $item['path'],
|
||||
'size' => 0, // GitLab doesn't provide size in tree API
|
||||
'type' => $this->get_file_type($item['name']),
|
||||
'url' => '' // Will need to construct if needed
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Bitbucket repository contents
|
||||
*/
|
||||
private function parse_bitbucket_contents($response, $repo) {
|
||||
$data = json_decode($response, true);
|
||||
$files = array();
|
||||
|
||||
if (!isset($data['values']) || !is_array($data['values'])) {
|
||||
return $files;
|
||||
}
|
||||
|
||||
$code_extensions = $this->get_code_extensions();
|
||||
|
||||
foreach ($data['values'] as $item) {
|
||||
if ($item['type'] === 'commit_file') {
|
||||
$extension = strtolower(pathinfo($item['path'], PATHINFO_EXTENSION));
|
||||
$name_lower = strtolower(basename($item['path']));
|
||||
|
||||
if (in_array($extension, $code_extensions) || $this->is_code_file($name_lower)) {
|
||||
$files[] = array(
|
||||
'name' => basename($item['path']),
|
||||
'path' => $item['path'],
|
||||
'size' => isset($item['size']) ? $item['size'] : 0,
|
||||
'type' => $this->get_file_type(basename($item['path'])),
|
||||
'url' => 'https://bitbucket.org/' . $repo . '/src/master/' . $item['path']
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of code file extensions
|
||||
*/
|
||||
private function get_code_extensions() {
|
||||
return array(
|
||||
'php', 'js', 'jsx', 'ts', 'tsx', 'py', 'rb', 'java', 'c', 'cpp', 'cc', 'cxx',
|
||||
'h', 'hpp', 'cs', 'swift', 'kt', 'go', 'rs', 'scala', 'r', 'sql', 'sh', 'bash',
|
||||
'yml', 'yaml', 'json', 'xml', 'html', 'css', 'scss', 'sass', 'less',
|
||||
'md', 'markdown', 'txt', 'ini', 'conf', 'dockerfile', 'makefile',
|
||||
'vue', 'svelte', 'elm', 'clj', 'ex', 'exs', 'erl', 'hrl', 'lua', 'pl', 'pm'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if filename indicates a code file
|
||||
*/
|
||||
private function is_code_file($filename) {
|
||||
$code_files = array(
|
||||
'dockerfile', 'makefile', '.gitignore', '.env', '.htaccess',
|
||||
'gemfile', 'rakefile', 'gulpfile', 'gruntfile', 'webpack.config.js',
|
||||
'package.json', 'composer.json', 'cargo.toml', 'go.mod'
|
||||
);
|
||||
|
||||
return in_array($filename, $code_files);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file type based on extension
|
||||
*/
|
||||
private function get_file_type($filename) {
|
||||
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||
$name_lower = strtolower($filename);
|
||||
|
||||
$type_map = array(
|
||||
'php' => 'php',
|
||||
'js' => 'javascript',
|
||||
'jsx' => 'jsx',
|
||||
'ts' => 'typescript',
|
||||
'tsx' => 'tsx',
|
||||
'py' => 'python',
|
||||
'rb' => 'ruby',
|
||||
'java' => 'java',
|
||||
'c' => 'c',
|
||||
'cpp' => 'cpp',
|
||||
'cc' => 'cpp',
|
||||
'cxx' => 'cpp',
|
||||
'h' => 'c',
|
||||
'hpp' => 'cpp',
|
||||
'cs' => 'csharp',
|
||||
'swift' => 'swift',
|
||||
'kt' => 'kotlin',
|
||||
'go' => 'go',
|
||||
'rs' => 'rust',
|
||||
'scala' => 'scala',
|
||||
'r' => 'r',
|
||||
'sql' => 'sql',
|
||||
'sh' => 'bash',
|
||||
'bash' => 'bash',
|
||||
'yml' => 'yaml',
|
||||
'yaml' => 'yaml',
|
||||
'json' => 'json',
|
||||
'xml' => 'xml',
|
||||
'html' => 'html',
|
||||
'css' => 'css',
|
||||
'scss' => 'scss',
|
||||
'sass' => 'sass',
|
||||
'less' => 'less',
|
||||
'md' => 'markdown',
|
||||
'markdown' => 'markdown'
|
||||
);
|
||||
|
||||
// Check special file names
|
||||
if ($name_lower === 'dockerfile') {
|
||||
return 'docker';
|
||||
}
|
||||
if ($name_lower === 'makefile') {
|
||||
return 'makefile';
|
||||
}
|
||||
|
||||
return isset($type_map[$extension]) ? $type_map[$extension] : 'plain';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if repository exists
|
||||
*/
|
||||
public function validate_repository_exists($repo_string) {
|
||||
$platform_info = $this->detect_platform($repo_string);
|
||||
$platform = $platform_info['platform'];
|
||||
$repo = $platform_info['repo'];
|
||||
|
||||
// Validate format first
|
||||
if (!preg_match($this->platforms[$platform]['repo_pattern'], $repo)) {
|
||||
return new WP_Error('invalid_repo', 'Invalid repository format');
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
$cache_key = 'mcb_repo_valid_' . $platform . '_' . md5($repo);
|
||||
$cached_result = get_transient($cache_key);
|
||||
|
||||
if ($cached_result !== false) {
|
||||
return $cached_result;
|
||||
}
|
||||
|
||||
// Check with platform API
|
||||
$exists = false;
|
||||
switch ($platform) {
|
||||
case 'github':
|
||||
$url = $this->platforms['github']['api_base'] . '/repos/' . $repo;
|
||||
break;
|
||||
case 'gitlab':
|
||||
$project_id = urlencode($repo);
|
||||
$url = $this->platforms['gitlab']['api_base'] . '/projects/' . $project_id;
|
||||
break;
|
||||
case 'bitbucket':
|
||||
$url = $this->platforms['bitbucket']['api_base'] . '/repositories/' . $repo;
|
||||
break;
|
||||
case 'codeberg':
|
||||
$url = $this->platforms['codeberg']['api_base'] . '/repos/' . $repo;
|
||||
break;
|
||||
default:
|
||||
return new WP_Error('unsupported_platform', 'Unsupported platform');
|
||||
}
|
||||
|
||||
$response = $this->make_api_request($url, $platform);
|
||||
|
||||
if (!is_wp_error($response)) {
|
||||
$data = json_decode($response, true);
|
||||
if (isset($data['id']) || isset($data['uuid']) || isset($data['name'])) {
|
||||
$exists = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($exists) {
|
||||
set_transient($cache_key, true, 3600);
|
||||
return true;
|
||||
}
|
||||
|
||||
return new WP_Error('repo_not_found', 'Repository not found or is private');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get platform display name
|
||||
*/
|
||||
public function get_platform_name($repo_string) {
|
||||
$platform_info = $this->detect_platform($repo_string);
|
||||
return $this->platforms[$platform_info['platform']]['name'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get repository web URL
|
||||
*/
|
||||
public function get_repository_url($repo_string) {
|
||||
$platform_info = $this->detect_platform($repo_string);
|
||||
$platform = $platform_info['platform'];
|
||||
$repo = $platform_info['repo'];
|
||||
|
||||
return $this->platforms[$platform]['web_base'] . '/' . $repo;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,324 @@
|
|||
<?php
|
||||
/**
|
||||
* Token Manager Class
|
||||
* Handles secure token storage with encryption
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('WPINC')) {
|
||||
die('Direct access not permitted.');
|
||||
}
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class MCB_Token_Manager {
|
||||
|
||||
/**
|
||||
* Check if encryption is available
|
||||
*/
|
||||
public static function encryption_available() {
|
||||
return function_exists('openssl_encrypt') && function_exists('openssl_decrypt');
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt token for secure storage
|
||||
*/
|
||||
public static function encrypt_token($token) {
|
||||
if (empty($token)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Use WordPress salts for encryption
|
||||
if (self::encryption_available()) {
|
||||
$key = substr(hash('sha256', wp_salt('auth')), 0, 32);
|
||||
$iv = substr(hash('sha256', wp_salt('secure')), 0, 16);
|
||||
|
||||
$encrypted = openssl_encrypt($token, 'AES-256-CBC', $key, 0, $iv);
|
||||
if ($encrypted !== false) {
|
||||
return base64_encode($encrypted);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to base64 if encryption not available
|
||||
return base64_encode($token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt token for use
|
||||
*/
|
||||
public static function decrypt_token($encrypted_token) {
|
||||
if (empty($encrypted_token)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Use WordPress salts for decryption
|
||||
if (self::encryption_available()) {
|
||||
$key = substr(hash('sha256', wp_salt('auth')), 0, 32);
|
||||
$iv = substr(hash('sha256', wp_salt('secure')), 0, 16);
|
||||
|
||||
$decoded = base64_decode($encrypted_token);
|
||||
if ($decoded !== false) {
|
||||
$decrypted = openssl_decrypt($decoded, 'AES-256-CBC', $key, 0, $iv);
|
||||
if ($decrypted !== false) {
|
||||
return $decrypted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to base64 if encryption not available
|
||||
return base64_decode($encrypted_token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store encrypted token
|
||||
*/
|
||||
public static function store_token($platform, $token) {
|
||||
if (empty($token)) {
|
||||
delete_option('mcb_' . $platform . '_token_encrypted');
|
||||
return true;
|
||||
}
|
||||
|
||||
$encrypted = self::encrypt_token($token);
|
||||
return update_option('mcb_' . $platform . '_token_encrypted', $encrypted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve and decrypt token
|
||||
*/
|
||||
public static function get_token($platform) {
|
||||
$encrypted = get_option('mcb_' . $platform . '_token_encrypted', '');
|
||||
if (empty($encrypted)) {
|
||||
// Check for legacy unencrypted token
|
||||
$legacy = get_option('mcb_' . $platform . '_token', '');
|
||||
if (!empty($legacy)) {
|
||||
// Migrate to encrypted storage
|
||||
self::store_token($platform, $legacy);
|
||||
delete_option('mcb_' . $platform . '_token');
|
||||
return $legacy;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
return self::decrypt_token($encrypted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all tokens (for privacy/GDPR)
|
||||
*/
|
||||
public static function remove_all_tokens() {
|
||||
$platforms = array('github', 'gitlab', 'bitbucket', 'codeberg');
|
||||
foreach ($platforms as $platform) {
|
||||
delete_option('mcb_' . $platform . '_token');
|
||||
delete_option('mcb_' . $platform . '_token_encrypted');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Privacy Manager Class
|
||||
* Handles GDPR compliance and privacy features
|
||||
*/
|
||||
class MCB_Privacy_Manager {
|
||||
|
||||
/**
|
||||
* Initialize privacy features
|
||||
*/
|
||||
public static function init() {
|
||||
// WordPress privacy policy content
|
||||
add_action('admin_init', array(__CLASS__, 'privacy_policy_content'));
|
||||
|
||||
// Data exporter for GDPR
|
||||
add_filter('wp_privacy_personal_data_exporters', array(__CLASS__, 'register_data_exporter'));
|
||||
|
||||
// Data eraser for GDPR
|
||||
add_filter('wp_privacy_personal_data_erasers', array(__CLASS__, 'register_data_eraser'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if privacy mode is enabled
|
||||
*/
|
||||
public static function is_privacy_mode() {
|
||||
return get_option('mcb_privacy_mode', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add suggested privacy policy content
|
||||
*/
|
||||
public static function privacy_policy_content() {
|
||||
if (!function_exists('wp_add_privacy_policy_content')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$content = '
|
||||
<h3>Git Code Viewer Plugin</h3>
|
||||
<p>When you visit pages that display code repositories using the Git Code Viewer plugin:</p>
|
||||
<ul>
|
||||
<li>We temporarily store an anonymized version of your IP address (for 60 seconds) to prevent abuse through rate limiting.</li>
|
||||
<li>If security events occur, we may log your browser user agent (for 7 days) for security monitoring.</li>
|
||||
<li>We do not use cookies or tracking technologies.</li>
|
||||
<li>We only access publicly available repository data from GitHub, GitLab, Bitbucket, and Codeberg.</li>
|
||||
<li>No personal information is shared with third parties.</li>
|
||||
<li>All data is automatically deleted after the retention period.</li>
|
||||
</ul>
|
||||
<p>If Privacy Mode is enabled, no IP addresses or user agents are collected.</p>
|
||||
';
|
||||
|
||||
wp_add_privacy_policy_content(
|
||||
'Git Code Viewer',
|
||||
wp_kses_post($content)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register data exporter for GDPR requests
|
||||
*/
|
||||
public static function register_data_exporter($exporters) {
|
||||
$exporters['git-code-viewer'] = array(
|
||||
'exporter_friendly_name' => __('Git Code Viewer Plugin'),
|
||||
'callback' => array(__CLASS__, 'data_exporter')
|
||||
);
|
||||
return $exporters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export user data for GDPR requests
|
||||
*/
|
||||
public static function data_exporter($email_address, $page = 1) {
|
||||
$export_items = array();
|
||||
|
||||
// Get security logs that might contain this user's data
|
||||
$logs = get_transient('mcb_security_logs');
|
||||
|
||||
if (is_array($logs)) {
|
||||
$user_data = array();
|
||||
|
||||
// Note: We don't store email addresses, so we can't directly match
|
||||
// This is actually good for privacy!
|
||||
$user_data[] = array(
|
||||
'name' => 'Security Logs Notice',
|
||||
'value' => 'The Git Code Viewer plugin does not store email addresses. Any security logs contain only anonymized IP addresses and user agents.'
|
||||
);
|
||||
|
||||
$export_items[] = array(
|
||||
'group_id' => 'git-code-viewer',
|
||||
'group_label' => 'Git Code Viewer Data',
|
||||
'item_id' => 'gcv-notice',
|
||||
'data' => $user_data
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'data' => $export_items,
|
||||
'done' => true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register data eraser for GDPR requests
|
||||
*/
|
||||
public static function register_data_eraser($erasers) {
|
||||
$erasers['git-code-viewer'] = array(
|
||||
'eraser_friendly_name' => __('Git Code Viewer Plugin'),
|
||||
'callback' => array(__CLASS__, 'data_eraser')
|
||||
);
|
||||
return $erasers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase user data for GDPR requests
|
||||
*/
|
||||
public static function data_eraser($email_address, $page = 1) {
|
||||
// Since we don't store email addresses or persistent user data,
|
||||
// we can only clear all transient data if requested
|
||||
|
||||
if ($page === 1) {
|
||||
// Clear all security logs
|
||||
delete_transient('mcb_security_logs');
|
||||
|
||||
// Clear all rate limit transients (they expire in 60 seconds anyway)
|
||||
global $wpdb;
|
||||
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_mcb_rate_%'");
|
||||
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_mcb_rate_%'");
|
||||
}
|
||||
|
||||
return array(
|
||||
'items_removed' => true,
|
||||
'items_retained' => false,
|
||||
'messages' => array('All temporary Git Code Viewer data has been removed.'),
|
||||
'done' => true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get anonymized IP for privacy compliance
|
||||
*/
|
||||
public static function get_anonymized_ip($ip) {
|
||||
if (self::is_privacy_mode()) {
|
||||
return 'privacy-mode';
|
||||
}
|
||||
|
||||
// Anonymize IP by removing last octet for IPv4 or last 80 bits for IPv6
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
$parts = explode('.', $ip);
|
||||
$parts[3] = '0';
|
||||
return implode('.', $parts);
|
||||
} elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
// Zero out last 80 bits
|
||||
return substr($ip, 0, strrpos($ip, ':')) . ':0:0:0:0:0';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old data automatically
|
||||
*/
|
||||
public static function cleanup_old_data() {
|
||||
// This is called on a daily schedule
|
||||
global $wpdb;
|
||||
|
||||
// Remove expired rate limit transients
|
||||
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_mcb_%' AND option_value < UNIX_TIMESTAMP()");
|
||||
|
||||
// Remove orphaned transients (fixed MySQL compatibility issue)
|
||||
// First get all valid transient names
|
||||
$valid_transients = $wpdb->get_col(
|
||||
"SELECT CONCAT('_transient_', SUBSTRING(option_name, 20))
|
||||
FROM {$wpdb->options}
|
||||
WHERE option_name LIKE '_transient_timeout_mcb_%'"
|
||||
);
|
||||
|
||||
if (!empty($valid_transients)) {
|
||||
// Build a safe IN clause
|
||||
$placeholders = array_fill(0, count($valid_transients), '%s');
|
||||
$in_clause = implode(',', $placeholders);
|
||||
|
||||
// Delete orphaned transients
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM {$wpdb->options}
|
||||
WHERE option_name LIKE '_transient_mcb_%'
|
||||
AND option_name NOT IN ($in_clause)",
|
||||
$valid_transients
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// No valid transients with timeouts, remove all MCB transients
|
||||
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_mcb_%'");
|
||||
}
|
||||
|
||||
// Also clean up expired timeout options
|
||||
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_mcb_%' AND option_value < UNIX_TIMESTAMP()");
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize privacy features
|
||||
MCB_Privacy_Manager::init();
|
||||
|
||||
// Schedule cleanup (weekly is sufficient for transient cleanup)
|
||||
if (!wp_next_scheduled('mcb_privacy_cleanup')) {
|
||||
wp_schedule_event(time(), 'weekly', 'mcb_privacy_cleanup');
|
||||
}
|
||||
add_action('mcb_privacy_cleanup', array('MCB_Privacy_Manager', 'cleanup_old_data'));
|
||||
|
|
@ -0,0 +1,385 @@
|
|||
<?php
|
||||
/**
|
||||
* Critical Security Fixes and Plugin Compatibility
|
||||
* Implementation of audit recommendations
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('WPINC')) {
|
||||
die('Direct access not permitted.');
|
||||
}
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class MCB_Security_Fixes {
|
||||
|
||||
/**
|
||||
* Initialize security fixes and compatibility patches
|
||||
*/
|
||||
public static function init() {
|
||||
// Fix 1: SSRF DNS Rebinding Protection
|
||||
add_filter('mcb_validate_api_url', array(__CLASS__, 'validate_api_url_dns'), 10, 2);
|
||||
|
||||
// Fix 2: Wordfence Compatibility
|
||||
add_action('init', array(__CLASS__, 'wordfence_compatibility'));
|
||||
|
||||
// Fix 3: Error Suppression in Production
|
||||
add_action('init', array(__CLASS__, 'suppress_errors_production'));
|
||||
|
||||
// Fix 4: Token Autoloading Fix
|
||||
add_filter('mcb_update_option', array(__CLASS__, 'disable_autoload'), 10, 3);
|
||||
|
||||
// Fix 5: User Agent Hashing
|
||||
add_filter('mcb_log_user_agent', array(__CLASS__, 'hash_user_agent'));
|
||||
|
||||
// Fix 6: Plugin Conflict Detection
|
||||
add_action('admin_notices', array(__CLASS__, 'compatibility_notices'));
|
||||
|
||||
// Fix 7: Performance Optimization
|
||||
add_action('init', array(__CLASS__, 'optimize_loading'));
|
||||
}
|
||||
|
||||
/**
|
||||
* CRITICAL FIX: SSRF DNS Rebinding Protection
|
||||
* Prevents accessing internal network resources
|
||||
*/
|
||||
public static function validate_api_url_dns($valid, $url) {
|
||||
$parsed = parse_url($url);
|
||||
if (!$parsed || !isset($parsed['host'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$host = $parsed['host'];
|
||||
|
||||
// Whitelist of allowed API hosts
|
||||
$allowed_hosts = array(
|
||||
'api.github.com',
|
||||
'gitlab.com',
|
||||
'api.bitbucket.org',
|
||||
'codeberg.org'
|
||||
);
|
||||
|
||||
if (!in_array($host, $allowed_hosts, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get IP address of the host
|
||||
$ip = gethostbyname($host);
|
||||
|
||||
// If resolution failed, block
|
||||
if ($ip === $host) {
|
||||
error_log('GCV Security: DNS resolution failed for ' . $host);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for private and reserved IP ranges
|
||||
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
|
||||
error_log('GCV Security: Blocked private/reserved IP ' . $ip . ' for host ' . $host);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Additional check for IPv6
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
// Check for IPv6 private ranges
|
||||
$private_ranges = array(
|
||||
'fc00::/7', // Unique local addresses
|
||||
'fe80::/10', // Link-local addresses
|
||||
'::1/128', // Loopback
|
||||
'::/128', // Unspecified
|
||||
'::ffff:0:0/96' // IPv4-mapped addresses
|
||||
);
|
||||
|
||||
foreach ($private_ranges as $range) {
|
||||
if (self::ip_in_range($ip, $range)) {
|
||||
error_log('GCV Security: Blocked private IPv6 ' . $ip);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wordfence Compatibility Layer
|
||||
*/
|
||||
public static function wordfence_compatibility() {
|
||||
if (!defined('WORDFENCE_VERSION')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add API URLs to Wordfence whitelist
|
||||
if (class_exists('wfConfig')) {
|
||||
$whitelisted = wfConfig::get('whitelisted');
|
||||
$whitelist_urls = array(
|
||||
'api.github.com',
|
||||
'gitlab.com',
|
||||
'api.bitbucket.org',
|
||||
'codeberg.org'
|
||||
);
|
||||
|
||||
foreach ($whitelist_urls as $url) {
|
||||
if (strpos($whitelisted, $url) === false) {
|
||||
$whitelisted .= "\n" . $url;
|
||||
}
|
||||
}
|
||||
|
||||
wfConfig::set('whitelisted', $whitelisted);
|
||||
}
|
||||
|
||||
// Disable plugin rate limiting if Wordfence is active
|
||||
if (has_filter('mcb_enable_rate_limiting')) {
|
||||
add_filter('mcb_enable_rate_limiting', '__return_false');
|
||||
}
|
||||
|
||||
// Add Wordfence compatibility headers
|
||||
add_action('mcb_before_api_request', function() {
|
||||
if (function_exists('wfUtils::doNotCache')) {
|
||||
wfUtils::doNotCache();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Suppress errors in production environments
|
||||
*/
|
||||
public static function suppress_errors_production() {
|
||||
// Only in production (non-debug mode)
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Suppress PHP errors in AJAX handlers
|
||||
if (wp_doing_ajax()) {
|
||||
@error_reporting(0);
|
||||
@ini_set('display_errors', '0');
|
||||
@ini_set('display_startup_errors', '0');
|
||||
|
||||
// Set custom error handler
|
||||
set_error_handler(function($errno, $errstr, $errfile, $errline) {
|
||||
// Log to error log but don't display
|
||||
if (WP_DEBUG_LOG) {
|
||||
error_log(sprintf(
|
||||
'GCV Error: %s in %s on line %d',
|
||||
$errstr,
|
||||
$errfile,
|
||||
$errline
|
||||
));
|
||||
}
|
||||
return true; // Prevent default error handler
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix token autoloading issue
|
||||
*/
|
||||
public static function disable_autoload($value, $option, $autoload) {
|
||||
// Disable autoload for token options
|
||||
if (strpos($option, 'mcb_') === 0 && strpos($option, '_token') !== false) {
|
||||
return 'no';
|
||||
}
|
||||
return $autoload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash user agents for privacy
|
||||
*/
|
||||
public static function hash_user_agent($user_agent) {
|
||||
if (empty($user_agent)) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
// Use a consistent salt for hashing
|
||||
$salt = wp_salt('auth');
|
||||
|
||||
// Create a hash that's consistent but not reversible
|
||||
return substr(hash('sha256', $salt . $user_agent), 0, 16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show compatibility notices
|
||||
*/
|
||||
public static function compatibility_notices() {
|
||||
$notices = array();
|
||||
|
||||
// Wordfence compatibility notice
|
||||
if (defined('WORDFENCE_VERSION')) {
|
||||
$notices[] = array(
|
||||
'type' => 'warning',
|
||||
'message' => __('Git Code Viewer: Wordfence detected. Please ensure the following URLs are whitelisted in Wordfence settings: api.github.com, gitlab.com, api.bitbucket.org, codeberg.org', 'maple-code-blocks'),
|
||||
'dismissible' => true
|
||||
);
|
||||
}
|
||||
|
||||
// WooCommerce compatibility (informational)
|
||||
if (class_exists('WooCommerce')) {
|
||||
// No issues, just log for debugging
|
||||
error_log('GCV: WooCommerce detected - compatibility mode active');
|
||||
}
|
||||
|
||||
// LearnDash compatibility (informational)
|
||||
if (defined('LEARNDASH_VERSION')) {
|
||||
// No issues, just log for debugging
|
||||
error_log('GCV: LearnDash detected - compatibility mode active');
|
||||
}
|
||||
|
||||
// Memory limit warning
|
||||
$memory_limit = ini_get('memory_limit');
|
||||
if ($memory_limit && self::convert_to_bytes($memory_limit) < 67108864) { // 64MB
|
||||
$notices[] = array(
|
||||
'type' => 'warning',
|
||||
'message' => __('Git Code Viewer: Low memory limit detected. Consider increasing memory_limit to at least 64MB for optimal performance.', 'maple-code-blocks'),
|
||||
'dismissible' => true
|
||||
);
|
||||
}
|
||||
|
||||
// Display notices
|
||||
foreach ($notices as $notice) {
|
||||
$dismissible = $notice['dismissible'] ? 'is-dismissible' : '';
|
||||
printf(
|
||||
'<div class="notice notice-%s %s"><p>%s</p></div>',
|
||||
esc_attr($notice['type']),
|
||||
esc_attr($dismissible),
|
||||
esc_html($notice['message'])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimize plugin loading
|
||||
*/
|
||||
public static function optimize_loading() {
|
||||
// Hook into admin_init for screen-specific optimizations
|
||||
if (is_admin()) {
|
||||
add_action('admin_init', function() {
|
||||
// Check screen only after it's available
|
||||
add_action('current_screen', function($screen) {
|
||||
if ($screen && $screen->id !== 'settings_page_maple-code-blocks') {
|
||||
remove_action('admin_enqueue_scripts', array('Maple_Code_Blocks', 'enqueue_scripts'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Optimize transient cleanup
|
||||
if (!wp_next_scheduled('mcb_cleanup_transients')) {
|
||||
wp_schedule_event(time(), 'daily', 'mcb_cleanup_transients');
|
||||
}
|
||||
|
||||
add_action('mcb_cleanup_transients', function() {
|
||||
global $wpdb;
|
||||
|
||||
// Use WordPress function instead of direct query
|
||||
if (function_exists('delete_expired_transients')) {
|
||||
delete_expired_transients();
|
||||
} else {
|
||||
// Fallback for older WordPress versions
|
||||
$wpdb->query(
|
||||
"DELETE FROM {$wpdb->options}
|
||||
WHERE option_name LIKE '_transient_timeout_mcb_%'
|
||||
AND option_value < UNIX_TIMESTAMP()"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Check if IP is in range
|
||||
*/
|
||||
private static function ip_in_range($ip, $range) {
|
||||
if (strpos($range, '/') === false) {
|
||||
$range .= '/32';
|
||||
}
|
||||
|
||||
list($subnet, $bits) = explode('/', $range);
|
||||
|
||||
// IPv4
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) &&
|
||||
filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
$ip = ip2long($ip);
|
||||
$subnet = ip2long($subnet);
|
||||
$mask = -1 << (32 - $bits);
|
||||
$subnet &= $mask;
|
||||
return ($ip & $mask) == $subnet;
|
||||
}
|
||||
|
||||
// IPv6
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) &&
|
||||
filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
$ip_bin = inet_pton($ip);
|
||||
$subnet_bin = inet_pton($subnet);
|
||||
$bytes = $bits / 8;
|
||||
$remainder = $bits % 8;
|
||||
|
||||
if ($bytes > 0) {
|
||||
if (substr($ip_bin, 0, $bytes) !== substr($subnet_bin, 0, $bytes)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($remainder > 0 && $bytes < 16) {
|
||||
$mask = 0xFF << (8 - $remainder);
|
||||
$ip_byte = ord($ip_bin[$bytes]);
|
||||
$subnet_byte = ord($subnet_bin[$bytes]);
|
||||
if (($ip_byte & $mask) !== ($subnet_byte & $mask)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Convert memory string to bytes
|
||||
*/
|
||||
private static function convert_to_bytes($val) {
|
||||
$val = trim($val);
|
||||
$last = strtolower($val[strlen($val)-1]);
|
||||
$val = (int)$val;
|
||||
|
||||
switch($last) {
|
||||
case 'g':
|
||||
$val *= 1024;
|
||||
case 'm':
|
||||
$val *= 1024;
|
||||
case 'k':
|
||||
$val *= 1024;
|
||||
}
|
||||
|
||||
return $val;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize security fixes
|
||||
add_action('plugins_loaded', array('MCB_Security_Fixes', 'init'), 5);
|
||||
add_action('rest_api_init', function() {
|
||||
register_rest_route('maple-code-blocks/v1', '/health', array(
|
||||
'methods' => 'GET',
|
||||
'callback' => function() {
|
||||
$health = array(
|
||||
'status' => 'healthy',
|
||||
'version' => '2.0.0',
|
||||
'php_version' => PHP_VERSION,
|
||||
'wordpress_version' => get_bloginfo('version'),
|
||||
'memory_usage' => memory_get_usage(true),
|
||||
'memory_limit' => ini_get('memory_limit'),
|
||||
'wordfence_active' => defined('WORDFENCE_VERSION'),
|
||||
'woocommerce_active' => class_exists('WooCommerce'),
|
||||
'learndash_active' => defined('LEARNDASH_VERSION'),
|
||||
'cache_size' => count(get_transient('mcb_security_logs') ?: array()),
|
||||
'rate_limit_active' => !defined('WORDFENCE_VERSION')
|
||||
);
|
||||
|
||||
return new WP_REST_Response($health, 200);
|
||||
},
|
||||
'permission_callback' => function() {
|
||||
return current_user_can('manage_options');
|
||||
}
|
||||
));
|
||||
});
|
||||
322
native/wordpress/maple-code-blocks/includes/class-security.php
Normal file
322
native/wordpress/maple-code-blocks/includes/class-security.php
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
<?php
|
||||
/**
|
||||
* Security Handler Class
|
||||
* Implements OWASP security best practices
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('WPINC')) {
|
||||
die('Direct access not permitted.');
|
||||
}
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class MCB_Security {
|
||||
|
||||
/**
|
||||
* Initialize security features
|
||||
*/
|
||||
public static function init() {
|
||||
// Add security headers
|
||||
add_action('send_headers', array(__CLASS__, 'add_security_headers'));
|
||||
|
||||
// Add Content Security Policy
|
||||
add_action('wp_head', array(__CLASS__, 'add_csp_meta'));
|
||||
|
||||
// Note: Global input sanitization removed - sanitize data at point of use instead
|
||||
// This prevents conflicts with other plugins (e.g., WPForms)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add security headers
|
||||
*/
|
||||
public static function add_security_headers() {
|
||||
// Only add headers on pages with our plugin
|
||||
if (!self::is_plugin_active_on_page()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// X-Content-Type-Options
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
|
||||
// X-Frame-Options
|
||||
header('X-Frame-Options: SAMEORIGIN');
|
||||
|
||||
// X-XSS-Protection (legacy but still useful)
|
||||
header('X-XSS-Protection: 1; mode=block');
|
||||
|
||||
// Referrer Policy
|
||||
header('Referrer-Policy: strict-origin-when-cross-origin');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Content Security Policy meta tag
|
||||
*/
|
||||
public static function add_csp_meta() {
|
||||
if (!self::is_plugin_active_on_page()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Strict CSP for code viewer areas
|
||||
$csp = "default-src 'self'; " .
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; " . // Needed for Prism.js
|
||||
"style-src 'self' 'unsafe-inline'; " . // Needed for inline styles
|
||||
"img-src 'self' data: https:; " .
|
||||
"connect-src 'self'; " .
|
||||
"font-src 'self' data:; " .
|
||||
"object-src 'none'; " .
|
||||
"base-uri 'self'; " .
|
||||
"form-action 'self'; " .
|
||||
"frame-ancestors 'self';";
|
||||
|
||||
echo '<meta http-equiv="Content-Security-Policy" content="' . esc_attr($csp) . '">' . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if plugin is active on current page
|
||||
*/
|
||||
private static function is_plugin_active_on_page() {
|
||||
global $post;
|
||||
|
||||
if (!is_a($post, 'WP_Post')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return has_shortcode($post->post_content, 'maple_code_block');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize global input arrays
|
||||
*/
|
||||
public static function sanitize_global_input() {
|
||||
// Sanitize $_GET
|
||||
if (!empty($_GET)) {
|
||||
$_GET = self::sanitize_array($_GET);
|
||||
}
|
||||
|
||||
// Sanitize $_POST
|
||||
if (!empty($_POST)) {
|
||||
$_POST = self::sanitize_array($_POST);
|
||||
}
|
||||
|
||||
// Sanitize $_REQUEST
|
||||
if (!empty($_REQUEST)) {
|
||||
$_REQUEST = self::sanitize_array($_REQUEST);
|
||||
}
|
||||
|
||||
// Sanitize $_COOKIE
|
||||
if (!empty($_COOKIE)) {
|
||||
$_COOKIE = self::sanitize_array($_COOKIE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively sanitize array
|
||||
*/
|
||||
private static function sanitize_array($array) {
|
||||
foreach ($array as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
$array[$key] = self::sanitize_array($value);
|
||||
} else {
|
||||
// Remove null bytes
|
||||
$value = str_replace(chr(0), '', $value);
|
||||
|
||||
// Strip tags and encode special chars
|
||||
$array[$key] = htmlspecialchars(strip_tags($value), ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate repository format
|
||||
* Supports: owner/repo, platform:owner/repo, or full URLs
|
||||
*/
|
||||
public static function validate_repo_format($repo) {
|
||||
// Remove any whitespace
|
||||
$repo = trim($repo);
|
||||
|
||||
// If it's a full URL, validate it
|
||||
if (strpos($repo, 'https://') === 0 || strpos($repo, 'http://') === 0) {
|
||||
// Check if it's from a supported platform
|
||||
$supported_domains = array(
|
||||
'github.com',
|
||||
'gitlab.com',
|
||||
'bitbucket.org',
|
||||
'codeberg.org'
|
||||
);
|
||||
|
||||
$parsed = parse_url($repo);
|
||||
if (!$parsed || !isset($parsed['host'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$domain_valid = false;
|
||||
foreach ($supported_domains as $domain) {
|
||||
if (strpos($parsed['host'], $domain) !== false) {
|
||||
$domain_valid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$domain_valid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract path and validate format
|
||||
if (!isset($parsed['path'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$path = trim($parsed['path'], '/');
|
||||
$parts = explode('/', $path);
|
||||
|
||||
// Need at least owner/repo
|
||||
if (count($parts) < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate owner and repo names
|
||||
$owner = $parts[0];
|
||||
$repo_name = $parts[1];
|
||||
|
||||
if (!preg_match('/^[a-zA-Z0-9\-_]+$/', $owner) ||
|
||||
!preg_match('/^[a-zA-Z0-9\-_\.]+$/', $repo_name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for platform prefix (e.g., gitlab:owner/repo)
|
||||
if (strpos($repo, ':') !== false) {
|
||||
list($platform, $repo_path) = explode(':', $repo, 2);
|
||||
|
||||
// Validate platform
|
||||
$valid_platforms = array('github', 'gitlab', 'bitbucket', 'codeberg');
|
||||
if (!in_array($platform, $valid_platforms)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate repo path
|
||||
if (!preg_match('/^[a-zA-Z0-9\-_]+\/[a-zA-Z0-9\-_\.]+$/', $repo_path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Standard format: owner/repo
|
||||
if (!preg_match('/^[a-zA-Z0-9\-_]+\/[a-zA-Z0-9\-_\.]+$/', $repo)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check length limits
|
||||
$parts = explode('/', $repo);
|
||||
if (count($parts) !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Owner: 1-39 characters (GitHub limit)
|
||||
if (strlen($parts[0]) < 1 || strlen($parts[0]) > 39) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Repo name: 1-100 characters (GitHub limit)
|
||||
if (strlen($parts[1]) < 1 || strlen($parts[1]) > 100) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate file path
|
||||
*/
|
||||
public static function validate_file_path($path) {
|
||||
// Remove leading/trailing slashes
|
||||
$path = trim($path, '/');
|
||||
|
||||
// Check for path traversal attempts
|
||||
$dangerous_patterns = array(
|
||||
'..',
|
||||
'//',
|
||||
'\\',
|
||||
'.git',
|
||||
'.env',
|
||||
'wp-config',
|
||||
'.htaccess',
|
||||
'.htpasswd'
|
||||
);
|
||||
|
||||
foreach ($dangerous_patterns as $pattern) {
|
||||
if (stripos($path, $pattern) !== false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Only allow safe characters
|
||||
if (!preg_match('/^[a-zA-Z0-9\-_\.\/]+$/', $path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check path depth (max 10 levels)
|
||||
if (substr_count($path, '/') > 10) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate secure random token
|
||||
*/
|
||||
public static function generate_token($length = 32) {
|
||||
if (function_exists('random_bytes')) {
|
||||
return bin2hex(random_bytes($length));
|
||||
} elseif (function_exists('openssl_random_pseudo_bytes')) {
|
||||
return bin2hex(openssl_random_pseudo_bytes($length));
|
||||
} else {
|
||||
// Fallback to less secure method
|
||||
return wp_generate_password($length * 2, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log security events
|
||||
*/
|
||||
public static function log_security_event($event_type, $details = array()) {
|
||||
$log_entry = array(
|
||||
'timestamp' => current_time('mysql'),
|
||||
'event_type' => $event_type,
|
||||
'ip_address' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '',
|
||||
'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '',
|
||||
'details' => $details
|
||||
);
|
||||
|
||||
// Store in WordPress transient for review (expires in 7 days)
|
||||
$logs = get_transient('mcb_security_logs');
|
||||
if (!is_array($logs)) {
|
||||
$logs = array();
|
||||
}
|
||||
|
||||
// Keep only last 100 entries
|
||||
if (count($logs) >= 100) {
|
||||
array_shift($logs);
|
||||
}
|
||||
|
||||
$logs[] = $log_entry;
|
||||
set_transient('mcb_security_logs', $logs, 7 * DAY_IN_SECONDS);
|
||||
|
||||
// For critical events, also log to error log
|
||||
if (in_array($event_type, array('invalid_nonce', 'rate_limit_exceeded', 'path_traversal_attempt'))) {
|
||||
error_log('GCV Security Event: ' . json_encode($log_entry));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize security features
|
||||
MCB_Security::init();
|
||||
190
native/wordpress/maple-code-blocks/includes/class-shortcode.php
Normal file
190
native/wordpress/maple-code-blocks/includes/class-shortcode.php
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
<?php
|
||||
/**
|
||||
* Shortcode Handler Class
|
||||
* Manages the [github_code_viewer] shortcode
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('WPINC')) {
|
||||
die('Direct access not permitted.');
|
||||
}
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class MCB_Shortcode {
|
||||
|
||||
/**
|
||||
* Initialize shortcode
|
||||
*/
|
||||
public static function init() {
|
||||
add_shortcode('maple_code_block', array(__CLASS__, 'render_shortcode'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the shortcode
|
||||
*/
|
||||
public static function render_shortcode($atts) {
|
||||
// Parse shortcode attributes
|
||||
$atts = shortcode_atts(array(
|
||||
'repo' => '',
|
||||
'theme' => 'dark',
|
||||
'height' => '600px',
|
||||
'show_line_numbers' => 'true',
|
||||
'initial_file' => '',
|
||||
'title' => ''
|
||||
), $atts, 'github_code_viewer');
|
||||
|
||||
// Validate repository
|
||||
if (empty($atts['repo'])) {
|
||||
return '<div class="mcb-error">Error: No repository specified. Use repo="owner/repository"</div>';
|
||||
}
|
||||
|
||||
// Sanitize attributes
|
||||
$repo = sanitize_text_field($atts['repo']);
|
||||
$theme = in_array($atts['theme'], array('dark', 'light', 'monokai', 'solarized')) ? $atts['theme'] : 'dark';
|
||||
|
||||
// Validate height - only allow specific units
|
||||
$height = sanitize_text_field($atts['height']);
|
||||
if (!preg_match('/^\d+(px|%|em|rem|vh)$/', $height)) {
|
||||
$height = '600px'; // Default to safe value if invalid
|
||||
}
|
||||
|
||||
$show_line_numbers = filter_var($atts['show_line_numbers'], FILTER_VALIDATE_BOOLEAN);
|
||||
$initial_file = sanitize_text_field($atts['initial_file']);
|
||||
$title = sanitize_text_field($atts['title']);
|
||||
|
||||
// Detect platform from repo string
|
||||
$platform = 'github'; // default
|
||||
$display_repo = $repo;
|
||||
|
||||
if (strpos($repo, ':') !== false) {
|
||||
list($platform, $display_repo) = explode(':', $repo, 2);
|
||||
} elseif (strpos($repo, 'https://') === 0 || strpos($repo, 'http://') === 0) {
|
||||
// Parse URL to detect platform
|
||||
if (strpos($repo, 'gitlab.com') !== false) {
|
||||
$platform = 'gitlab';
|
||||
} elseif (strpos($repo, 'bitbucket.org') !== false) {
|
||||
$platform = 'bitbucket';
|
||||
} elseif (strpos($repo, 'codeberg.org') !== false) {
|
||||
$platform = 'codeberg';
|
||||
}
|
||||
// Extract repo name from URL
|
||||
$parsed = parse_url($repo);
|
||||
if ($parsed && isset($parsed['path'])) {
|
||||
$display_repo = trim($parsed['path'], '/');
|
||||
}
|
||||
}
|
||||
|
||||
// Generate unique ID for this instance
|
||||
$viewer_id = 'mcb-' . uniqid();
|
||||
|
||||
// Build the viewer HTML
|
||||
ob_start();
|
||||
?>
|
||||
<div id="<?php echo esc_attr($viewer_id); ?>" class="maple-code-blocks mcb-theme-<?php echo esc_attr($theme); ?>"
|
||||
data-repo="<?php echo esc_attr($repo); ?>"
|
||||
data-theme="<?php echo esc_attr($theme); ?>"
|
||||
data-show-line-numbers="<?php echo $show_line_numbers ? 'true' : 'false'; ?>"
|
||||
data-initial-file="<?php echo esc_attr($initial_file); ?>"
|
||||
style="height: <?php echo esc_attr($height); ?>;">
|
||||
|
||||
<div class="mcb-header">
|
||||
<?php if (!empty($title)) : ?>
|
||||
<h3 class="mcb-title"><?php echo esc_html($title); ?></h3>
|
||||
<?php endif; ?>
|
||||
<div class="mcb-repo-info">
|
||||
<?php
|
||||
// Display platform-specific icon
|
||||
switch($platform) {
|
||||
case 'gitlab':
|
||||
?>
|
||||
<svg class="mcb-platform-icon" viewBox="0 0 24 24" width="20" height="20">
|
||||
<path fill="#FC6D26" d="M22.65 14.39L12 22.13 1.35 14.39a.84.84 0 0 1-.3-.94l1.22-3.78 2.44-7.51A.42.42 0 0 1 4.82 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.49h8.1l2.44-7.51A.42.42 0 0 1 18.6 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.51L23 13.45a.84.84 0 0 1-.35.94z"/>
|
||||
</svg>
|
||||
<?php
|
||||
break;
|
||||
case 'bitbucket':
|
||||
?>
|
||||
<svg class="mcb-platform-icon" viewBox="0 0 24 24" width="20" height="20">
|
||||
<path fill="#0052CC" d="M3.28 2.42a1 1 0 00-.97.8l-2.3 14.3a1.35 1.35 0 00.78 1.51l9.84 4.13c.36.15.77.15 1.13 0l9.86-4.13a1.35 1.35 0 00.78-1.5l-2.3-14.31a1 1 0 00-.97-.8H3.28zm8.3 12.66h-3.8l-1-6.4h5.56l.75 6.4z"/>
|
||||
</svg>
|
||||
<?php
|
||||
break;
|
||||
case 'codeberg':
|
||||
?>
|
||||
<svg class="mcb-platform-icon" viewBox="0 0 24 24" width="20" height="20">
|
||||
<path fill="#2185D0" d="M12 2C6.48 2 2 6.48 2 12c0 4.42 2.87 8.17 6.84 9.49.5.09.68-.22.68-.48 0-.24-.01-1.02-.01-1.85-2.78.6-3.37-1.18-3.37-1.18-.45-1.15-1.11-1.46-1.11-1.46-.91-.62.07-.61.07-.61 1 .07 1.53 1.03 1.53 1.03.89 1.53 2.34 1.09 2.91.83.09-.64.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.93 0-1.09.39-1.98 1.03-2.68-.1-.25-.45-1.27.1-2.64 0 0 .84-.27 2.75 1.03A9.58 9.58 0 0112 6.8c.85.004 1.71.115 2.51.34 1.91-1.3 2.75-1.03 2.75-1.03.55 1.37.2 2.39.1 2.64.64.7 1.03 1.59 1.03 2.68 0 3.83-2.34 4.68-4.56 4.92.36.31.68.92.68 1.85 0 1.34-.01 2.42-.01 2.75 0 .27.18.58.69.48A10.01 10.01 0 0022 12c0-5.52-4.48-10-10-10z"/>
|
||||
</svg>
|
||||
<?php
|
||||
break;
|
||||
case 'github':
|
||||
default:
|
||||
?>
|
||||
<svg class="mcb-platform-icon" viewBox="0 0 24 24" width="20" height="20">
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||||
</svg>
|
||||
<?php
|
||||
break;
|
||||
}
|
||||
?>
|
||||
<span class="mcb-repo-name"><?php echo esc_html($display_repo); ?></span>
|
||||
</div>
|
||||
<div class="mcb-controls">
|
||||
<button class="mcb-home-btn" title="Go to root">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18">
|
||||
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="mcb-refresh-btn" title="Refresh files">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18">
|
||||
<path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="mcb-fullscreen-btn" title="Toggle fullscreen">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18">
|
||||
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mcb-content">
|
||||
<div class="mcb-sidebar">
|
||||
<div class="mcb-search-box">
|
||||
<input type="text" class="mcb-search-input" placeholder="Search files...">
|
||||
</div>
|
||||
<div class="mcb-file-list">
|
||||
<div class="mcb-loading">
|
||||
<div class="mcb-spinner"></div>
|
||||
<span>Loading repository files...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mcb-editor">
|
||||
<div class="mcb-tabs">
|
||||
<!-- Tabs will be added dynamically -->
|
||||
</div>
|
||||
<div class="mcb-code-area">
|
||||
<div class="mcb-welcome">
|
||||
<svg viewBox="0 0 24 24" width="64" height="64">
|
||||
<path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/>
|
||||
</svg>
|
||||
<h4>GitHub Code Viewer</h4>
|
||||
<p>Select a file from the sidebar to view its content</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mcb-status-bar">
|
||||
<span class="mcb-status-text">Ready</span>
|
||||
<span class="mcb-file-info"></span>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
/**
|
||||
* Simplified Gutenberg Block Registration for Maple Code Blocks
|
||||
*/
|
||||
|
||||
class MCB_Simple_Block {
|
||||
|
||||
public static function init() {
|
||||
// Register block on init
|
||||
add_action('init', array(__CLASS__, 'register_block'));
|
||||
|
||||
// Make sure script is enqueued in editor
|
||||
add_action('enqueue_block_editor_assets', array(__CLASS__, 'enqueue_editor_assets'));
|
||||
}
|
||||
|
||||
public static function register_block() {
|
||||
// Only register if Gutenberg is available
|
||||
if (!function_exists('register_block_type')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Register the block
|
||||
register_block_type('maple-code-blocks/code-block', array(
|
||||
'render_callback' => array(__CLASS__, 'render_block'),
|
||||
'attributes' => array(
|
||||
'repository' => array(
|
||||
'type' => 'string',
|
||||
'default' => ''
|
||||
),
|
||||
'theme' => array(
|
||||
'type' => 'string',
|
||||
'default' => 'dark'
|
||||
),
|
||||
'height' => array(
|
||||
'type' => 'string',
|
||||
'default' => '600px'
|
||||
)
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
public static function enqueue_editor_assets() {
|
||||
// Enqueue the block editor script
|
||||
wp_enqueue_script(
|
||||
'maple-code-blocks-editor',
|
||||
MCB_PLUGIN_URL . 'assets/js/simple-block.js',
|
||||
array('wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n'),
|
||||
MCB_PLUGIN_VERSION,
|
||||
false // Load in header, not footer
|
||||
);
|
||||
|
||||
// Add inline script to ensure registration happens
|
||||
wp_add_inline_script('maple-code-blocks-editor', '
|
||||
console.log("Maple Code Blocks: Script loaded");
|
||||
if (typeof wp !== "undefined" && wp.blocks) {
|
||||
console.log("Maple Code Blocks: wp.blocks is available");
|
||||
} else {
|
||||
console.error("Maple Code Blocks: wp.blocks not available");
|
||||
}
|
||||
', 'after');
|
||||
}
|
||||
|
||||
public static function render_block($attributes) {
|
||||
$repository = isset($attributes['repository']) ? $attributes['repository'] : '';
|
||||
$theme = isset($attributes['theme']) ? $attributes['theme'] : 'dark';
|
||||
$height = isset($attributes['height']) ? $attributes['height'] : '600px';
|
||||
|
||||
if (empty($repository)) {
|
||||
return '<div class="notice notice-warning">Please enter a repository (e.g., facebook/react)</div>';
|
||||
}
|
||||
|
||||
// Use the shortcode for rendering
|
||||
return do_shortcode(sprintf(
|
||||
'[maple_code_block repo="%s" theme="%s" height="%s"]',
|
||||
esc_attr($repository),
|
||||
esc_attr($theme),
|
||||
esc_attr($height)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
MCB_Simple_Block::init();
|
||||
2
native/wordpress/maple-code-blocks/includes/index.php
Normal file
2
native/wordpress/maple-code-blocks/includes/index.php
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?php
|
||||
// Silence is golden.
|
||||
398
native/wordpress/maple-code-blocks/includes/security-audit.php
Normal file
398
native/wordpress/maple-code-blocks/includes/security-audit.php
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
<?php
|
||||
/**
|
||||
* Security Audit Script for GitHub Code Viewer Plugin
|
||||
* Run this to verify all security measures are in place
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('WPINC')) {
|
||||
die('Direct access not permitted.');
|
||||
}
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class MCB_Security_Audit {
|
||||
|
||||
private $issues = array();
|
||||
private $passed = array();
|
||||
|
||||
/**
|
||||
* Run complete security audit
|
||||
*/
|
||||
public function run_audit() {
|
||||
$this->check_file_permissions();
|
||||
$this->check_index_files();
|
||||
$this->check_htaccess_files();
|
||||
$this->check_php_files_protection();
|
||||
$this->check_input_validation();
|
||||
$this->check_output_escaping();
|
||||
$this->check_nonce_usage();
|
||||
$this->check_capability_checks();
|
||||
$this->check_ssl_usage();
|
||||
$this->check_rate_limiting();
|
||||
|
||||
return $this->generate_report();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check file permissions
|
||||
*/
|
||||
private function check_file_permissions() {
|
||||
$plugin_dir = plugin_dir_path(__FILE__);
|
||||
|
||||
// Check directory permissions (should be 755 or stricter)
|
||||
if (is_readable($plugin_dir)) {
|
||||
$perms = fileperms($plugin_dir);
|
||||
$octal = substr(sprintf('%o', $perms), -3);
|
||||
|
||||
if ($octal > '755') {
|
||||
$this->issues[] = 'Directory permissions too permissive: ' . $octal;
|
||||
} else {
|
||||
$this->passed[] = 'Directory permissions OK: ' . $octal;
|
||||
}
|
||||
}
|
||||
|
||||
// Check file permissions (should be 644 or stricter)
|
||||
$files = glob($plugin_dir . '*.php');
|
||||
foreach ($files as $file) {
|
||||
$perms = fileperms($file);
|
||||
$octal = substr(sprintf('%o', $perms), -3);
|
||||
|
||||
if ($octal > '644') {
|
||||
$this->issues[] = 'File permissions too permissive for ' . basename($file) . ': ' . $octal;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($this->issues)) {
|
||||
$this->passed[] = 'All file permissions are secure';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for index.php files in all directories
|
||||
*/
|
||||
private function check_index_files() {
|
||||
$plugin_dir = plugin_dir_path(__FILE__);
|
||||
$directories = array(
|
||||
$plugin_dir,
|
||||
$plugin_dir . 'admin/',
|
||||
$plugin_dir . 'includes/',
|
||||
$plugin_dir . 'assets/',
|
||||
$plugin_dir . 'assets/css/',
|
||||
$plugin_dir . 'assets/js/'
|
||||
);
|
||||
|
||||
foreach ($directories as $dir) {
|
||||
if (is_dir($dir) && !file_exists($dir . 'index.php')) {
|
||||
$this->issues[] = 'Missing index.php in ' . str_replace($plugin_dir, '', $dir);
|
||||
} else {
|
||||
$this->passed[] = 'index.php present in ' . str_replace($plugin_dir, '', $dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for .htaccess files
|
||||
*/
|
||||
private function check_htaccess_files() {
|
||||
$plugin_dir = plugin_dir_path(__FILE__);
|
||||
$required_htaccess = array(
|
||||
$plugin_dir => 'root',
|
||||
$plugin_dir . 'admin/' => 'admin',
|
||||
$plugin_dir . 'includes/' => 'includes',
|
||||
$plugin_dir . 'assets/' => 'assets'
|
||||
);
|
||||
|
||||
foreach ($required_htaccess as $dir => $name) {
|
||||
if (file_exists($dir . '.htaccess')) {
|
||||
$this->passed[] = '.htaccess present in ' . $name . ' directory';
|
||||
} else {
|
||||
$this->issues[] = 'Missing .htaccess in ' . $name . ' directory';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check PHP files have direct access protection
|
||||
*/
|
||||
private function check_php_files_protection() {
|
||||
$plugin_dir = plugin_dir_path(__FILE__);
|
||||
$php_files = $this->get_all_php_files($plugin_dir);
|
||||
|
||||
foreach ($php_files as $file) {
|
||||
$content = file_get_contents($file);
|
||||
|
||||
// Skip index.php files
|
||||
if (basename($file) === 'index.php') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for ABSPATH or WPINC checks
|
||||
if (!strpos($content, 'ABSPATH') && !strpos($content, 'WPINC')) {
|
||||
$this->issues[] = 'No direct access protection in ' . str_replace($plugin_dir, '', $file);
|
||||
} else {
|
||||
$this->passed[] = 'Direct access protected: ' . basename($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check input validation
|
||||
*/
|
||||
private function check_input_validation() {
|
||||
$checks = array(
|
||||
'sanitize_text_field' => 'Text sanitization',
|
||||
'wp_verify_nonce' => 'Nonce verification',
|
||||
'esc_attr' => 'Attribute escaping',
|
||||
'esc_html' => 'HTML escaping',
|
||||
'esc_js' => 'JavaScript escaping',
|
||||
'absint' => 'Integer validation',
|
||||
'filter_var' => 'Input filtering'
|
||||
);
|
||||
|
||||
$plugin_dir = plugin_dir_path(__FILE__);
|
||||
$php_files = $this->get_all_php_files($plugin_dir);
|
||||
|
||||
foreach ($checks as $function => $description) {
|
||||
$found = false;
|
||||
foreach ($php_files as $file) {
|
||||
$content = file_get_contents($file);
|
||||
if (strpos($content, $function) !== false) {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($found) {
|
||||
$this->passed[] = $description . ' implemented (' . $function . ')';
|
||||
} else {
|
||||
$this->issues[] = $description . ' not found (' . $function . ')';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check output escaping
|
||||
*/
|
||||
private function check_output_escaping() {
|
||||
$escaping_functions = array(
|
||||
'esc_html',
|
||||
'esc_attr',
|
||||
'esc_url',
|
||||
'esc_js',
|
||||
'htmlspecialchars'
|
||||
);
|
||||
|
||||
$plugin_dir = plugin_dir_path(__FILE__);
|
||||
$php_files = $this->get_all_php_files($plugin_dir);
|
||||
|
||||
$escaping_found = false;
|
||||
foreach ($php_files as $file) {
|
||||
$content = file_get_contents($file);
|
||||
foreach ($escaping_functions as $func) {
|
||||
if (strpos($content, $func) !== false) {
|
||||
$escaping_found = true;
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($escaping_found) {
|
||||
$this->passed[] = 'Output escaping implemented';
|
||||
} else {
|
||||
$this->issues[] = 'No output escaping functions found';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check nonce usage
|
||||
*/
|
||||
private function check_nonce_usage() {
|
||||
$nonce_functions = array(
|
||||
'wp_create_nonce',
|
||||
'wp_verify_nonce',
|
||||
'wp_nonce_field',
|
||||
'check_admin_referer'
|
||||
);
|
||||
|
||||
$plugin_dir = plugin_dir_path(__FILE__);
|
||||
$php_files = $this->get_all_php_files($plugin_dir);
|
||||
|
||||
$nonce_found = 0;
|
||||
foreach ($php_files as $file) {
|
||||
$content = file_get_contents($file);
|
||||
foreach ($nonce_functions as $func) {
|
||||
if (strpos($content, $func) !== false) {
|
||||
$nonce_found++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($nonce_found >= 2) { // Should have both create and verify
|
||||
$this->passed[] = 'Nonce protection implemented';
|
||||
} else {
|
||||
$this->issues[] = 'Insufficient nonce protection';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check capability checks
|
||||
*/
|
||||
private function check_capability_checks() {
|
||||
$capability_functions = array(
|
||||
'current_user_can',
|
||||
'user_can',
|
||||
'is_admin'
|
||||
);
|
||||
|
||||
$plugin_dir = plugin_dir_path(__FILE__);
|
||||
$php_files = $this->get_all_php_files($plugin_dir);
|
||||
|
||||
$capability_found = false;
|
||||
foreach ($php_files as $file) {
|
||||
$content = file_get_contents($file);
|
||||
foreach ($capability_functions as $func) {
|
||||
if (strpos($content, $func) !== false) {
|
||||
$capability_found = true;
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($capability_found) {
|
||||
$this->passed[] = 'Capability checks implemented';
|
||||
} else {
|
||||
$this->issues[] = 'No capability checks found';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check SSL usage
|
||||
*/
|
||||
private function check_ssl_usage() {
|
||||
$plugin_dir = plugin_dir_path(__FILE__);
|
||||
$php_files = $this->get_all_php_files($plugin_dir);
|
||||
|
||||
$https_enforced = false;
|
||||
foreach ($php_files as $file) {
|
||||
$content = file_get_contents($file);
|
||||
if (strpos($content, 'https://api.github.com') !== false) {
|
||||
$https_enforced = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($https_enforced) {
|
||||
$this->passed[] = 'HTTPS enforced for API calls';
|
||||
} else {
|
||||
$this->issues[] = 'HTTPS not enforced for API calls';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check rate limiting
|
||||
*/
|
||||
private function check_rate_limiting() {
|
||||
$plugin_dir = plugin_dir_path(__FILE__);
|
||||
$php_files = $this->get_all_php_files($plugin_dir);
|
||||
|
||||
$rate_limiting = false;
|
||||
foreach ($php_files as $file) {
|
||||
$content = file_get_contents($file);
|
||||
if (strpos($content, 'rate_limit') !== false || strpos($content, 'throttl') !== false) {
|
||||
$rate_limiting = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($rate_limiting) {
|
||||
$this->passed[] = 'Rate limiting implemented';
|
||||
} else {
|
||||
$this->issues[] = 'No rate limiting found';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all PHP files recursively
|
||||
*/
|
||||
private function get_all_php_files($dir) {
|
||||
$files = array();
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($dir)
|
||||
);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile() && $file->getExtension() === 'php') {
|
||||
$files[] = $file->getPathname();
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate audit report
|
||||
*/
|
||||
private function generate_report() {
|
||||
$report = array(
|
||||
'timestamp' => current_time('mysql'),
|
||||
'passed_count' => count($this->passed),
|
||||
'issues_count' => count($this->issues),
|
||||
'passed' => $this->passed,
|
||||
'issues' => $this->issues,
|
||||
'score' => $this->calculate_score(),
|
||||
'status' => empty($this->issues) ? 'SECURE' : 'NEEDS ATTENTION'
|
||||
);
|
||||
|
||||
return $report;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate security score
|
||||
*/
|
||||
private function calculate_score() {
|
||||
$total = count($this->passed) + count($this->issues);
|
||||
if ($total === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return round((count($this->passed) / $total) * 100);
|
||||
}
|
||||
}
|
||||
|
||||
// Run audit if requested
|
||||
if (isset($_GET['mcb_security_audit']) && current_user_can('manage_options')) {
|
||||
$audit = new MCB_Security_Audit();
|
||||
$report = $audit->run_audit();
|
||||
|
||||
echo '<div class="wrap">';
|
||||
echo '<h1>GitHub Code Viewer Security Audit</h1>';
|
||||
echo '<div class="notice notice-' . (empty($report['issues']) ? 'success' : 'warning') . '">';
|
||||
echo '<p><strong>Security Score: ' . $report['score'] . '%</strong></p>';
|
||||
echo '<p>Status: ' . $report['status'] . '</p>';
|
||||
echo '</div>';
|
||||
|
||||
if (!empty($report['passed'])) {
|
||||
echo '<h2>✅ Passed Checks (' . $report['passed_count'] . ')</h2>';
|
||||
echo '<ul>';
|
||||
foreach ($report['passed'] as $pass) {
|
||||
echo '<li style="color: green;">✓ ' . esc_html($pass) . '</li>';
|
||||
}
|
||||
echo '</ul>';
|
||||
}
|
||||
|
||||
if (!empty($report['issues'])) {
|
||||
echo '<h2>⚠️ Issues Found (' . $report['issues_count'] . ')</h2>';
|
||||
echo '<ul>';
|
||||
foreach ($report['issues'] as $issue) {
|
||||
echo '<li style="color: red;">✗ ' . esc_html($issue) . '</li>';
|
||||
}
|
||||
echo '</ul>';
|
||||
}
|
||||
|
||||
echo '<p><em>Audit completed at ' . $report['timestamp'] . '</em></p>';
|
||||
echo '</div>';
|
||||
}
|
||||
2
native/wordpress/maple-code-blocks/index.php
Normal file
2
native/wordpress/maple-code-blocks/index.php
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?php
|
||||
// Silence is golden.
|
||||
286
native/wordpress/maple-code-blocks/maple-code-blocks.php
Normal file
286
native/wordpress/maple-code-blocks/maple-code-blocks.php
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
<?php
|
||||
/**
|
||||
* Plugin Name: Maple Code Blocks
|
||||
* Plugin URI: https://sspmedia.ca/wordpress/
|
||||
* Description: Display code files from GitHub, GitLab, Bitbucket, and Codeberg repositories in a beautiful, safe terminal/IDE-style view
|
||||
* Version: 2.0.0
|
||||
* Author: SSP Media
|
||||
* Author URI: https://sspmedia.ca/wordpress/
|
||||
* License: GPL v2 or later
|
||||
* Text Domain: maple-code-blocks
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('WPINC')) {
|
||||
die('Direct access not permitted.');
|
||||
}
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Define plugin constants
|
||||
define('MCB_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||
define('MCB_PLUGIN_PATH', plugin_dir_path(__FILE__));
|
||||
define('MCB_PLUGIN_VERSION', '2.0.0');
|
||||
define('MCB_PLUGIN_BASENAME', plugin_basename(__FILE__));
|
||||
|
||||
// Include required files
|
||||
require_once MCB_PLUGIN_PATH . 'includes/class-security.php';
|
||||
require_once MCB_PLUGIN_PATH . 'includes/class-security-fixes.php';
|
||||
require_once MCB_PLUGIN_PATH . 'includes/class-privacy-manager.php';
|
||||
require_once MCB_PLUGIN_PATH . 'includes/class-github-api.php';
|
||||
require_once MCB_PLUGIN_PATH . 'includes/class-code-renderer.php';
|
||||
require_once MCB_PLUGIN_PATH . 'includes/class-shortcode.php';
|
||||
require_once MCB_PLUGIN_PATH . 'includes/class-simple-block.php';
|
||||
require_once MCB_PLUGIN_PATH . 'includes/basic-block.php';
|
||||
// require_once MCB_PLUGIN_PATH . 'includes/class-block-editor.php';
|
||||
// require_once MCB_PLUGIN_PATH . 'includes/class-block-patterns.php';
|
||||
|
||||
// Initialize plugin
|
||||
class Maple_Code_Blocks {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance() {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function __construct() {
|
||||
add_action('init', array($this, 'init'));
|
||||
add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts'));
|
||||
add_action('wp_ajax_mcb_load_file', array($this, 'ajax_load_file'));
|
||||
add_action('wp_ajax_nopriv_mcb_load_file', array($this, 'ajax_load_file'));
|
||||
add_action('wp_ajax_mcb_get_repo_files', array($this, 'ajax_get_repo_files'));
|
||||
add_action('wp_ajax_nopriv_mcb_get_repo_files', array($this, 'ajax_get_repo_files'));
|
||||
}
|
||||
|
||||
public function init() {
|
||||
// Initialize shortcode
|
||||
MCB_Shortcode::init();
|
||||
|
||||
// Simple block is self-initializing via class-simple-block.php
|
||||
|
||||
// Add admin menu if needed
|
||||
if (is_admin()) {
|
||||
add_action('admin_menu', array($this, 'add_admin_menu'));
|
||||
}
|
||||
}
|
||||
|
||||
public function enqueue_scripts() {
|
||||
// Load styles
|
||||
wp_enqueue_style('prism-css', MCB_PLUGIN_URL . 'assets/css/prism.css', array(), MCB_PLUGIN_VERSION);
|
||||
wp_enqueue_style('mcb-styles', MCB_PLUGIN_URL . 'assets/css/mcb-styles.css', array(), MCB_PLUGIN_VERSION);
|
||||
|
||||
// Load scripts
|
||||
wp_enqueue_script('prism-core', MCB_PLUGIN_URL . 'assets/js/prism.js', array(), MCB_PLUGIN_VERSION, true);
|
||||
wp_enqueue_script('mcb-script', MCB_PLUGIN_URL . 'assets/js/mcb-script.js', array('jquery'), MCB_PLUGIN_VERSION, true);
|
||||
|
||||
// Localize script for AJAX
|
||||
wp_localize_script('mcb-script', 'mcb_ajax', array(
|
||||
'ajax_url' => admin_url('admin-ajax.php'),
|
||||
'nonce' => wp_create_nonce('mcb_ajax_nonce')
|
||||
));
|
||||
}
|
||||
|
||||
public function ajax_load_file() {
|
||||
// Check request method
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
wp_die('Invalid request method', 405);
|
||||
}
|
||||
|
||||
// Rate limiting check
|
||||
if (class_exists('MCB_Privacy_Manager') && MCB_Privacy_Manager::is_privacy_mode()) {
|
||||
// Use nonce-based rate limiting in privacy mode
|
||||
$rate_limit_key = 'mcb_rate_' . wp_get_current_user()->ID . '_' . wp_create_nonce('mcb_rate');
|
||||
} else {
|
||||
// IP-based rate limiting
|
||||
$user_ip = $this->get_client_ip();
|
||||
$rate_limit_key = 'mcb_rate_' . md5($user_ip);
|
||||
}
|
||||
|
||||
$requests = get_transient($rate_limit_key);
|
||||
|
||||
if ($requests !== false && $requests > 30) { // 30 requests per minute
|
||||
wp_die('Rate limit exceeded. Please try again later.', 429);
|
||||
}
|
||||
|
||||
set_transient($rate_limit_key, ($requests ? $requests + 1 : 1), 60);
|
||||
|
||||
// Verify nonce
|
||||
if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mcb_ajax_nonce')) {
|
||||
wp_die('Security check failed', 403);
|
||||
}
|
||||
|
||||
// Note: We allow public viewing of public repositories by default
|
||||
// Site admins can restrict access if needed via filter
|
||||
$require_login = apply_filters('mcb_require_login_for_viewing', false);
|
||||
|
||||
if ($require_login && !is_user_logged_in()) {
|
||||
wp_die('Please log in to view code repositories', 403);
|
||||
}
|
||||
|
||||
$repo = isset($_POST['repo']) ? sanitize_text_field($_POST['repo']) : '';
|
||||
$file_path = isset($_POST['file_path']) ? sanitize_text_field($_POST['file_path']) : '';
|
||||
|
||||
// Additional validation using security class
|
||||
if (!MCB_Security::validate_repo_format($repo)) {
|
||||
MCB_Security::log_security_event('invalid_repo_format', array('repo' => $repo));
|
||||
wp_send_json_error('Invalid repository format');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!MCB_Security::validate_file_path($file_path)) {
|
||||
MCB_Security::log_security_event('invalid_file_path', array('path' => $file_path));
|
||||
wp_send_json_error('Invalid file path');
|
||||
return;
|
||||
}
|
||||
|
||||
$github_api = new MCB_GitHub_API();
|
||||
$content = $github_api->get_file_content($repo, $file_path);
|
||||
|
||||
if (is_wp_error($content)) {
|
||||
wp_send_json_error($content->get_error_message());
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate content before sending
|
||||
$renderer = new MCB_Code_Renderer();
|
||||
$validation = $renderer->validate_content($content);
|
||||
|
||||
if (is_wp_error($validation)) {
|
||||
wp_send_json_error($validation->get_error_message());
|
||||
return;
|
||||
}
|
||||
|
||||
// Return raw content - JavaScript will handle escaping and rendering
|
||||
// This is more secure as it avoids double-escaping issues
|
||||
wp_send_json_success(array(
|
||||
'content' => $content, // Send raw content, not rendered HTML
|
||||
'filename' => basename($file_path)
|
||||
));
|
||||
}
|
||||
|
||||
public function ajax_get_repo_files() {
|
||||
// Check request method
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
wp_die('Invalid request method', 405);
|
||||
}
|
||||
|
||||
// Rate limiting check
|
||||
$user_ip = $this->get_client_ip();
|
||||
$rate_limit_key = 'mcb_rate_' . md5($user_ip);
|
||||
$requests = get_transient($rate_limit_key);
|
||||
|
||||
if ($requests !== false && $requests > 30) { // 30 requests per minute
|
||||
wp_die('Rate limit exceeded. Please try again later.', 429);
|
||||
}
|
||||
|
||||
set_transient($rate_limit_key, ($requests ? $requests + 1 : 1), 60);
|
||||
|
||||
// Verify nonce
|
||||
if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mcb_ajax_nonce')) {
|
||||
wp_die('Security check failed', 403);
|
||||
}
|
||||
|
||||
// Note: We allow public viewing of public repositories by default
|
||||
// Site admins can restrict access if needed via filter
|
||||
$require_login = apply_filters('mcb_require_login_for_viewing', false);
|
||||
|
||||
if ($require_login && !is_user_logged_in()) {
|
||||
wp_die('Please log in to view code repositories', 403);
|
||||
}
|
||||
|
||||
$repo = isset($_POST['repo']) ? sanitize_text_field($_POST['repo']) : '';
|
||||
$path = isset($_POST['path']) ? sanitize_text_field($_POST['path']) : '';
|
||||
|
||||
// Validate repository format
|
||||
if (!MCB_Security::validate_repo_format($repo)) {
|
||||
wp_send_json_error('Invalid repository format');
|
||||
return;
|
||||
}
|
||||
|
||||
$github_api = new MCB_GitHub_API();
|
||||
$files = $github_api->get_repository_files($repo, $path);
|
||||
|
||||
if (is_wp_error($files)) {
|
||||
wp_send_json_error($files->get_error_message());
|
||||
return;
|
||||
}
|
||||
|
||||
wp_send_json_success($files);
|
||||
}
|
||||
|
||||
public function add_admin_menu() {
|
||||
add_options_page(
|
||||
'GitHub Code Viewer Settings',
|
||||
'GitHub Code Viewer',
|
||||
'manage_options',
|
||||
'maple-code-blocks',
|
||||
array($this, 'admin_page')
|
||||
);
|
||||
}
|
||||
|
||||
public function admin_page() {
|
||||
include MCB_PLUGIN_PATH . 'admin/settings-page.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get client IP address for rate limiting
|
||||
*/
|
||||
private function get_client_ip() {
|
||||
$ip_keys = array('HTTP_CF_CONNECTING_IP', 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR');
|
||||
foreach ($ip_keys as $key) {
|
||||
if (array_key_exists($key, $_SERVER) === true) {
|
||||
$ips = explode(',', $_SERVER[$key]);
|
||||
foreach ($ips as $ip) {
|
||||
$ip = trim($ip);
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP,
|
||||
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) {
|
||||
return $ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0';
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the plugin
|
||||
Maple_Code_Blocks::get_instance();
|
||||
|
||||
// Register deactivation hook to clean up scheduled events
|
||||
register_deactivation_hook(__FILE__, 'mcb_deactivate');
|
||||
function mcb_deactivate() {
|
||||
// Remove scheduled cleanup event
|
||||
$timestamp = wp_next_scheduled('mcb_privacy_cleanup');
|
||||
if ($timestamp) {
|
||||
wp_unschedule_event($timestamp, 'mcb_privacy_cleanup');
|
||||
}
|
||||
|
||||
// Clear all MCB transients
|
||||
global $wpdb;
|
||||
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_mcb_%'");
|
||||
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_mcb_%'");
|
||||
}
|
||||
|
||||
// Register uninstall hook for complete cleanup
|
||||
register_uninstall_hook(__FILE__, 'mcb_uninstall');
|
||||
function mcb_uninstall() {
|
||||
// Remove all plugin options
|
||||
delete_option('mcb_settings');
|
||||
delete_option('mcb_github_token_encrypted');
|
||||
delete_option('mcb_gitlab_token_encrypted');
|
||||
delete_option('mcb_bitbucket_token_encrypted');
|
||||
delete_option('mcb_codeberg_token_encrypted');
|
||||
|
||||
// Clear all transients
|
||||
global $wpdb;
|
||||
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_mcb_%'");
|
||||
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_mcb_%'");
|
||||
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_site_transient_mcb_%'");
|
||||
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_site_transient_timeout_mcb_%'");
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue