update(doc): added spanish & chinese

This commit is contained in:
rooyca 2026-03-31 03:42:07 -05:00
parent 4a9670c80a
commit bab5f0de7d
21 changed files with 2729 additions and 391 deletions

371
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,371 @@
# Contributing to API Request Plugin
Thank you for your interest in contributing to the API Request plugin! This document provides guidelines and information for contributors.
## Table of Contents
- [Code of Conduct](#code-of-conduct)
- [Getting Started](#getting-started)
- [Development Setup](#development-setup)
- [Making Changes](#making-changes)
- [Code Style](#code-style)
- [Testing](#testing)
- [Submitting Changes](#submitting-changes)
- [Reporting Bugs](#reporting-bugs)
- [Requesting Features](#requesting-features)
## Code of Conduct
Please read and follow our [Code of Conduct](CODE_OF_CONDUCT.md).
## Getting Started
1. Fork the repository
2. Clone your fork: `git clone https://github.com/YOUR_USERNAME/obsidian-api-request.git`
3. Create a branch: `git checkout -b feature/your-feature-name`
## Development Setup
### Prerequisites
- Node.js (v16 or higher)
- npm (v7 or higher)
- Obsidian (for testing)
### Installation
```bash
# Install dependencies
npm install
# Build the plugin
npm run build
# Development mode (auto-rebuild on changes)
npm run dev
```
### Project Structure
```
obsidian-api-request/
├── src/
│ ├── main.ts # Main plugin file
│ ├── functions/
│ │ ├── general.ts # General utility functions
│ │ ├── regx.ts # Regular expressions
│ │ ├── security.ts # Security utilities
│ │ └── frontmatterUtils.ts # Frontmatter parsing
│ └── settings/
│ ├── settingsData.ts # Settings interface
│ └── settingsTab.ts # Settings UI
├── docs/ # Documentation
├── main.js # Compiled output
├── manifest.json # Plugin manifest
└── package.json # Dependencies
```
## Making Changes
### Branch Naming
- `feature/` - New features
- `fix/` - Bug fixes
- `docs/` - Documentation updates
- `refactor/` - Code refactoring
- `security/` - Security improvements
### Commit Messages
Follow conventional commits format:
```
type(scope): brief description
Longer description if needed
- Additional details
- More details
```
Types:
- `feat`: New feature
- `fix`: Bug fix
- `docs`: Documentation changes
- `refactor`: Code refactoring
- `perf`: Performance improvements
- `test`: Adding or updating tests
- `security`: Security improvements
Examples:
```
feat(request): add support for PATCH method
fix(cache): prevent race condition in localStorage
docs(readme): add security best practices section
security(validation): sanitize UUID input to prevent injection
```
## Code Style
### TypeScript
- Use TypeScript for all new code
- Add proper type annotations
- Avoid `any` type when possible
- Use interfaces for complex types
### Documentation
- Add JSDoc comments to all public functions
- Include `@param`, `@returns`, `@throws` tags
- Add `@example` for complex functions
- Document security considerations with `@security` tag
Example:
```typescript
/**
* Validates a URL to ensure it's safe to request
*
* @param url - The URL to validate
* @returns true if valid, false otherwise
* @security Only allows http:// and https:// protocols
* @example
* isValidUrl("https://api.example.com") // returns true
* isValidUrl("file:///etc/passwd") // returns false
*/
export function isValidUrl(url: string): boolean {
// implementation
}
```
### Code Organization
- Keep functions focused and single-purpose
- Extract magic numbers to constants
- Use descriptive variable names
- Limit function length to ~50 lines
### Error Handling
- Always wrap risky operations in try-catch
- Log errors to console with context
- Show user-friendly error messages via Notice
- Never expose sensitive data in errors
Example:
```typescript
try {
const data = localStorage.getItem(key);
if (data) {
return safeJsonParse(data);
}
} catch (e: any) {
console.error("Error reading from localStorage:", e);
new Notice("Error: Failed to retrieve cached data");
return null;
}
```
## Testing
### Manual Testing
1. Build the plugin: `npm run build`
2. Copy `main.js`, `manifest.json`, and `styles.css` to your Obsidian vault's plugins folder
3. Reload Obsidian
4. Test your changes thoroughly
### Test Cases to Verify
- URL validation with various protocols
- UUID sanitization with special characters
- JSONPath injection attempts
- File path traversal attempts
- XSS prevention in format strings
- Error handling for network failures
- localStorage cache operations
- Variable substitution (global, frontmatter, localStorage)
### Security Testing
Before submitting security-related changes:
1. Test with malicious inputs
2. Verify input sanitization
3. Check for XSS vulnerabilities
4. Test error handling edge cases
5. Review console logs for sensitive data leaks
## Submitting Changes
### Pull Request Process
1. Update documentation if needed
2. Add yourself to contributors if first contribution
3. Ensure the build passes: `npm run build`
4. Update CHANGELOG.md if significant change
5. Create a pull request with:
- Clear title describing the change
- Description of what and why
- Related issue numbers
- Screenshots for UI changes
- Testing steps
### Pull Request Template
```markdown
## Description
Brief description of changes
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
- [ ] Security improvement
## Testing
Steps to test the changes
## Checklist
- [ ] Code follows project style guidelines
- [ ] Added/updated documentation
- [ ] Tested manually
- [ ] No console errors
- [ ] Updated CHANGELOG.md
```
### Review Process
- Maintainers will review your PR
- Address feedback and requested changes
- Once approved, your PR will be merged
## Reporting Bugs
### Before Reporting
1. Check existing issues
2. Verify you're using the latest version
3. Reproduce the bug with minimal configuration
### Bug Report Template
```markdown
**Description**
Clear description of the bug
**To Reproduce**
Steps to reproduce:
1. Go to '...'
2. Click on '...'
3. See error
**Expected Behavior**
What should happen
**Actual Behavior**
What actually happens
**Screenshots**
If applicable
**Environment**
- Plugin version:
- Obsidian version:
- OS:
**Additional Context**
Any other relevant information
```
## Requesting Features
### Feature Request Template
```markdown
**Problem Statement**
What problem does this solve?
**Proposed Solution**
How should it work?
**Alternatives Considered**
Other approaches considered
**Additional Context**
Examples, mockups, related issues
```
## Security Issues
**Do not report security vulnerabilities in public issues!**
See [SECURITY.md](SECURITY.md) for how to report security issues.
## Code Review Guidelines
When reviewing code:
1. **Functionality**: Does it work as intended?
2. **Security**: Are inputs validated? Any XSS risks?
3. **Performance**: Any unnecessary operations?
4. **Maintainability**: Is the code clear and documented?
5. **Consistency**: Does it follow project conventions?
## Development Tips
### Hot Reload
For faster development:
1. Run `npm run dev` to watch for changes
2. Use Obsidian's developer tools (Ctrl+Shift+I)
3. Reload plugin: Ctrl+R in dev tools
### Debugging
```typescript
// Add debug logging
console.log("Debug:", variable);
// Use Obsidian's Notice for user feedback
new Notice("Debug: Operation completed");
// Use debugger breakpoints
debugger;
```
### Common Issues
**Build fails**:
- Check TypeScript errors: `npm run build`
- Verify all imports are correct
- Check for missing type definitions
**Plugin not loading**:
- Verify manifest.json is correct
- Check Obsidian console for errors
- Ensure minAppVersion matches your Obsidian
**Changes not appearing**:
- Rebuild: `npm run build`
- Reload Obsidian: Ctrl+R
- Check file is copied to correct location
## Questions?
- Open a discussion on GitHub
- Ask in issues (for bug-related questions)
- Check existing documentation
## Recognition
Contributors will be:
- Listed in the project README
- Mentioned in release notes
- Credited in the plugin description
Thank you for contributing! 🎉

View file

@ -8,9 +8,18 @@
[![English](https://img.shields.io/badge/English-8A2BE2)](README.md)
[![中文](https://img.shields.io/badge/中文-8A2BE2)](README.zh.md)
Este plugin para [Obsidian](https://obsidian.md/) permite a los usuarios realizar solicitudes HTTP desde sus notas y mostrar la respuesta en un bloque de código, una ventana modal o pegarla directamente en su documento actual.
![req_img](showcase_1.gif)
Este plugin para [Obsidian](https://obsidian.md/) permite a los usuarios realizar solicitudes API directamente desde sus notas y mostrar las respuestas en bloques de código.
## 🔒 Seguridad
Este plugin implementa medidas de seguridad completas que incluyen:
- Validación de entradas para URLs, rutas y datos de usuario
- Prevención de XSS mediante sanitización de HTML
- Protección contra ataques de directory traversal
- Evaluación segura de expresiones JSONPath
- Manejo seguro de localStorage
## 🚀 Instalación
@ -30,13 +39,47 @@ Este plugin se puede instalar desde Obsidian.
![showcase](showcase_2.gif)
## ✅ To-do
## ✅ Por hacer
- [x] Añadir más tipos de solicitudes (POST, PUT, DELETE)
- [x] Añadir soporte para autenticación
- [x] Guardar la respuesta en un archivo
- [x] Eliminar uno a uno del localStorage
- [ ] Traducir la documentación
> Consulta todos los cambios en [TODOS-v2.md](TODOS-v2.md)
- [ ] Traducir (y actualizar) documentación
- [x] Reutilización de datos (sintaxis `{{ls.UUID>JSONPath}}`, donde `ls` significa `localStorage`)
- [x] Soporte para comentarios usando sintaxis `#` o `//`
- [x] Consulta en línea desde la respuesta (usando **Dataview**)
- [ ] Añadir pruebas (!!!)
- [ ] Re-implementar flag `repeat` (repetir solicitudes X veces o cada X segundos)
- [x] Mejoras de seguridad (validación de entradas, prevención de XSS, protección contra directory traversal)
- [x] Mejoras de seguridad de tipos TypeScript
- [x] Manejo completo de errores
## 🤝 Contribuir
¡Las contribuciones son bienvenidas! Por favor, lee nuestras [Guías de Contribución](CONTRIBUTING.md) antes de enviar un pull request.
### Desarrollo
```bash
# Instalar dependencias
npm install
# Compilar
npm run build
# Modo desarrollo (auto-recompilación)
npm run dev
```
Para más detalles, consulta [CONTRIBUTING.md](CONTRIBUTING.md).
## 🔒 Mejores Prácticas de Seguridad
Al usar este plugin:
1. **Usa HTTPS**: Siempre usa URLs HTTPS para solicitudes API
2. **Protege las Claves API**: Almacena las claves API en variables globales, nunca en las notas
3. **Revisa Datos en Caché**: Limpia regularmente respuestas antiguas en caché
4. **Valida Fuentes**: Solo conéctate a endpoints API de confianza
## ❤️ Patrocinadores
@ -44,6 +87,6 @@ Este plugin se puede instalar desde Obsidian.
## ✍️ Comentarios y Contribuciones
Si encuentras algún problema o tienes comentarios sobre el plugin, no dudes en abrir un problema en el [repositorio de GitHub](https://github.com/Rooyca/obsidian-api-request).
Si encuentras algún problema o tienes comentarios sobre el plugin, no dudes en abrir un issue en el [repositorio de GitHub](https://github.com/Rooyca/obsidian-api-request).
¡Las contribuciones también son bienvenidas!
¡Las contribuciones también son bienvenidas!

View file

@ -11,6 +11,15 @@
This [Obsidian](https://obsidian.md/) plugin enables users to make API requests directly within their notes and display the response in a code-block.
## 🔒 Security
This plugin implements comprehensive security measures including:
- Input validation for URLs, paths, and user data
- XSS prevention through HTML sanitization
- Protection against directory traversal attacks
- Safe JSONPath expression evaluation
- Secure localStorage handling
## 🚀 Installation
The plugin can be installed from within Obsidian.
@ -37,6 +46,38 @@ The plugin can be installed from within Obsidian.
- [x] Inline query from response (using **Dataview**)
- [ ] Add tests (!!!)
- [ ] Re-implement `repeat` flag (repeat requests X times or every X seconds)
- [x] Security improvements (input validation, XSS prevention, path traversal protection)
- [x] TypeScript type safety improvements
- [x] Comprehensive error handling
## 🤝 Contributing
Contributions are welcome! Please read our [Contributing Guidelines](CONTRIBUTING.md) before submitting a pull request.
### Development
```bash
# Install dependencies
npm install
# Build
npm run build
# Development mode (auto-rebuild)
npm run dev
```
For more details, see [CONTRIBUTING.md](CONTRIBUTING.md).
## 🔒 Security Best Practices
When using this plugin:
1. **Use HTTPS**: Always use HTTPS URLs for API requests
2. **Protect API Keys**: Store API keys in global variables, not in notes
3. **Review Cached Data**: Regularly clear old cached responses
4. **Validate Sources**: Only connect to trusted API endpoints
## ❤️ Sponsors

View file

@ -9,39 +9,82 @@
[![English](https://img.shields.io/badge/English-8A2BE2)](README.md)
这个[Obsidian](https://obsidian.md/)插件能让用户直接在笔记中进行 HTTP 请求,并在代码块、模式窗口中显示响应,或直接将其粘贴到活动文档中。
这个[Obsidian](https://obsidian.md/)插件能让用户直接在笔记中进行 API 请求,并在代码块中显示响应。
## 🔒 安全性
本插件实现了全面的安全措施,包括:
- URL、路径和用户数据的输入验证
- 通过 HTML 清理防止 XSS 攻击
- 防止目录遍历攻击
- 安全的 JSONPath 表达式评估
- 安全的 localStorage 处理
## 🚀 安装
该插件可以从 Obsidian 内部安装。
### Obsidian 社区插件浏览器
- 转到 `设置` -> `社区插件`
- 确保 `受限模式` 已 **关闭**
- 点击 `浏览`
- 搜索 `APIRequest`
- 点击 `安装`,然后点击 `启用`
## 🛠️ 使用
### [阅读文档](https://rooyca.github.io/obsidian-api-request/)
## ✅ 待办事项
> 查看 [TODOS-v2.md](TODOS-v2.md) 中的所有更改
- [ ] 翻译(和更新)文档
- [x] 数据重用(`{{ls.UUID>JSONPath}}` 语法,其中 `ls` 代表 `localStorage`
- [x] 支持使用 `#``//` 语法的注释
- [x] 从响应中进行内联查询(使用 **Dataview**
- [ ] 添加测试(!!!
- [ ] 重新实现 `repeat` 标志(重复请求 X 次或每 X 秒)
- [x] 安全改进输入验证、XSS 防护、目录遍历保护)
- [x] TypeScript 类型安全改进
- [x] 全面的错误处理
## 🤝 贡献
欢迎贡献!在提交 pull request 之前,请阅读我们的[贡献指南](CONTRIBUTING.md)。
### 开发
```bash
# 安装依赖
npm install
# 构建
npm run build
# 开发模式(自动重建)
npm run dev
```
有关更多详细信息,请参阅 [CONTRIBUTING.md](CONTRIBUTING.md)。
## 🔒 安全最佳实践
使用此插件时:
1. **使用 HTTPS**:进行 API 请求时始终使用 HTTPS URL
2. **保护 API 密钥**:将 API 密钥存储在全局变量中,而不是在笔记中
3. **审查缓存数据**:定期清除旧的缓存响应
4. **验证来源**:仅连接到受信任的 API 端点
## 🚀 Installation
The plugin can be installed from within Obsidian.
### Obsidian Community Plugin Browser
- Go to `Settings` -> `Community plugins`
- Make sure `Restricted mode` is **off**
- Click `Browse`
- Search for `APIRequest`
- Click `Install` and then `Enable`
## 🛠️ Usage
### [Read the docs](https://rooyca.github.io/obsidian-api-request/)
## ✅ To-do
- [ ] Translate documentation
- [x] Data re-usage (`{{ls.UUID>JSONPath}}` syntax, where `ls` stands for `localStorage`)
- [x] Support for comments using `#` or `//` syntax
- [ ] Inline query from response
- [ ] Add tests (!!!)
- [ ] Re-implement `repeat` flag (repeat requests X times or every X seconds)
- [ ] Re-implement `properties` flag (specifies the frontmatter properties to update with the response)
## ❤️ Sponsors
## ❤️ 赞助商
<a href="https://github.com/tlwt"><img src="https://github.com/tlwt.png" width="40px" /></a>
## ✍️ Feedback and Contributions
## ✍️ 反馈和贡献
If you encounter any issues or have feedback on the plugin, feel free to open an issue on the [GitHub repository](https://github.com/Rooyca/obsidian-api-request). Contributions are also welcome!
如果您遇到任何问题或对插件有反馈,请随时在 [GitHub 仓库](https://github.com/Rooyca/obsidian-api-request)上提出问题。
也欢迎贡献!

View file

@ -1,28 +1,50 @@
# 👨🏻‍💻 Codeblocks
The `codeblock` is a versatile block that can be used to write code in different languages. In this case, we will use it to make API requests.
The `codeblock` is a versatile block that can be used to make API requests directly from your notes. This guide covers all available flags and how to use them effectively and securely.
## 🏳️ Flags
Flags are the way to specify the parameters of our request.
Flags are the way to specify the parameters of our request. All flags are **case-insensitive**.
| Flag | Default |
| ------------| ---------|
| [url](#url) | |
| [method](#method) | GET |
| [body](#body) | |
| [headers](#headers) | |
| [show](#show) | ALL |
| [req-uuid](#req-uuid) | req-general |
| [hidden](#hidden) | FALSE |
| [disabled](#disabled) | |
| [save-as](#save-as) | |
| [auto-update](#auto-update) | FALSE |
| [format](#format) | |
| [properties](#properties) | |
| Flag | Default | Required | Description |
| ------------| ---------| -------- | ----------- |
| [url](#url) | | ✅ Yes | The API endpoint to request |
| [method](#method) | GET | No | HTTP method (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS) |
| [body](#body) | | No | Request body (JSON format) |
| [headers](#headers) | | No | Request headers (JSON format) |
| [show](#show) | ALL | No | JSONPath to extract specific data |
| [req-uuid](#req-uuid) | req-general | No | Unique ID for caching |
| [hidden](#hidden) | FALSE | No | Hide the output |
| [disabled](#disabled) | | No | Disable the request |
| [save-as](#save-as) | | No | Save response to file |
| [auto-update](#auto-update) | FALSE | No | Always fetch fresh data |
| [format](#format) | | No | Custom output format |
| [properties](#properties) | | No | Update frontmatter properties |
---
## 🔒 Security Considerations
!!! danger "Important Security Guidelines"
- **Always use HTTPS**: Use `https://` URLs to ensure encrypted communication
- **Protect API Keys**: Store sensitive tokens in global variables, never hardcode them in notes
- **Validate Inputs**: The plugin validates all inputs, but only connect to trusted APIs
- **Review Cached Data**: Regularly clear old cached responses from Settings
### Input Validation
All user inputs are automatically validated:
- **URLs**: Only `http://` and `https://` protocols are allowed
- **UUIDs**: Sanitized to alphanumeric, hyphens, and underscores only
- **File Paths**: Directory traversal (`..`) and absolute paths are blocked
- **JSONPath**: Expressions are validated to prevent script injection
- **Format Strings**: HTML is sanitized to prevent XSS attacks
---
## 📖 Variables & Data Reuse
### LocalStorage & Variables
API responses can be stored in `localStorage` and reused in other codeblocks or notes. To store a response, you must assign it a unique identifier using the `req-uuid` flag.
@ -33,36 +55,99 @@ You can access stored responses using the following syntax:
{{ls.UUID>JSONPath}}
```
* `UUID`: The unique identifier defined in the `req-uuid` flag.
* `JSONPath`: The path to the specific data you want from the response.
* `UUID`: The unique identifier defined in the `req-uuid` flag (without the `req-` prefix)
* `JSONPath`: The path to the specific data you want from the response
**Example:**
If you have a request with `req-uuid: user`, you can access the users name like this:
If you have a request with `req-uuid: user`, you can access the user's name like this:
```
{{ls.user>$.name}}
```
!!! info "Security Note"
UUIDs are automatically sanitized. Only alphanumeric characters, hyphens, and underscores are allowed.
---
You can also reference variables defined in the note's **frontmatter** using:
### Frontmatter Variables
You can reference variables defined in the note's **frontmatter** using:
```
{{this.variableName}}
```
**Example:**
Frontmatter:
```yaml
---
userId: 12345
title: "My Note"
---
```
Usage in request:
~~~markdown
```req
url: https://api.example.com/user/{{this.userId}}
```
~~~
!!! warning "Security Warning"
Avoid storing sensitive API keys in frontmatter. Use global variables instead.
---
For **global variables**, you can define them in the plugin settings. These are saved in `localStorage` and can be accessed with:
### Global Variables
Define reusable variables in plugin settings (Settings → APIRequest → Global variables). These are stored securely and can be accessed with:
```
{{ls.variableName}}
{{VARIABLE_NAME}}
```
**Example:**
1. In plugin settings, add:
- Key: `API_KEY`
- Value: `your-secret-key`
2. In your request:
~~~markdown
```req
url: https://api.example.com/data
headers: {"Authorization": "Bearer {{API_KEY}}"}
```
~~~
!!! tip "Best Practice"
Store all API keys and tokens as global variables to keep your notes secure and portable.
---
### Special Variables
- `{{this.file.name}}` - Current file's basename (without extension)
---
## Flag Details
### url
Is the only **required** flag. It specifies the endpoint of the request.
The only **required** flag. It specifies the endpoint of the API request.
**Syntax:**
~~~markdown
```req
url: https://api.example.com/endpoint
```
~~~
**With variables:**
~~~markdown
```req
# this is just a comment
@ -72,16 +157,25 @@ url: https://jsonplaceholder.typicode.com/users/{{this.id}}
!!! note "Where `{{this.id}}` is a variable (`id`) defined in the frontmatter."
!!! danger "Security"
Only HTTPS and HTTP URLs are allowed. Other protocols (file://, javascript:, etc.) are blocked for security.
---
### method
Specifies the request method. The default value is `GET` and the available methods are:
Specifies the HTTP request method. The default value is `GET`.
- GET
**Available methods:**
- GET (default)
- POST
- PUT
- DELETE
- PATCH
- HEAD
- OPTIONS
**Example:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/posts
@ -89,24 +183,40 @@ method: post
```
~~~
---
### body
Specifies the body of the request. The default value is an empty object. The data should be in JSON format, separating key and value with a colon plus space (`, `).
Specifies the body of the request. The data should be in JSON format.
**Example:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/posts
method: post
body: {"title": {{this.filename}}, "body": "bar", "userId": 1}
body: {"title": "{{this.filename}}", "body": "bar", "userId": 1}
```
~~~
!!! note "Where `{{this.filename}}` is the name of the working file."
**Advanced usage with variables:**
~~~markdown
```req
url: https://api.example.com/create
method: post
body: {"userId": {{this.userId}}, "token": "{{API_TOKEN}}"}
```
~~~
---
### headers
Specifies the headers of the request. The default value is an empty object. The data should be in JSON format, separating key and value with a colon plus space (`, `).
Specifies the request headers. The data should be in JSON format.
**Example:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/posts
@ -115,23 +225,32 @@ headers: {"Content-type": "application/json; charset=UTF-8"}
```
~~~
**Using cached responses as headers:**
You can use responses from other requests as headers/body/url/show. For example, if you have a request with `req-uuid: token`, you can use it like this:
~~~makdown
~~~markdown
```req
url: https://api.todoist.com/rest/v2/tasks
headers: {"Authorization": "Bearer {{ls.token>$.access_token}}"}
show: $..content
format: - [ ] {}
req-id: todos
req-uuid: todos
```
~~~
!!! tip "Common Headers"
- `Content-Type: application/json` - For JSON data
- `Authorization: Bearer TOKEN` - For API authentication
- `Accept: application/json` - To request JSON responses
---
### show
Specifies the response data to display. See [JSONPath examples](https://github.com/JSONPath-Plus/JSONPath?tab=readme-ov-file#syntax-through-examples), or try the online tool by [jsonpath-plus](https://jsonpath-plus.github.io/JSONPath/demo/).
Specifies which data from the response to display using JSONPath expressions. See [JSONPath examples](https://github.com/JSONPath-Plus/JSONPath?tab=readme-ov-file#syntax-through-examples), or try the online tool by [jsonpath-plus](https://jsonpath-plus.github.io/JSONPath/demo/).
**Simple example:**
~~~markdown
```req
url: https://api.chess.com/pub/player/hikaru/stats
@ -139,8 +258,7 @@ show: $['chess_daily']['last']['rating']
```
~~~
Multiple outputs can be displayed by using `[]`.
**Multiple outputs using brackets:**
~~~markdown
```req
url: https://api.chess.com/pub/player/hikaru/stats
@ -148,8 +266,7 @@ show: $.chess_daily[last,best].rating
```
~~~
Or you can also use `+` to get multiple outputs.
**Multiple outputs using +:**
~~~markdown
```req
url: https://api.chess.com/pub/player/hikaru/stats
@ -157,8 +274,9 @@ show: $.chess_daily[last,best].rating + $.chess960_daily[last,best].rating
```
~~~
Looping over an array is also possible. The following example retrieves the city from all users.
**Looping over arrays:**
Get city from all users:
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/users
@ -166,8 +284,7 @@ show: $..address.city
```
~~~
Looping over a specified number of elements of the array is also possible.
**Looping over first N elements:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/users
@ -175,8 +292,7 @@ show: $..[:3].address.city
```
~~~
It's also possible to loop over a specified range of indexes of the array.
**Specific array indexes:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/users
@ -184,8 +300,9 @@ show: $..[3,2,6].address.city
```
~~~
You can access the last element using `(@.length-1)` or just `[-1:]`.
**Last element:**
You can access the last element using `(@.length-1)` or just `[-1:]`:
~~~markdown
```req
url: https://api.modrinth.com/v2/project/distanthorizons
@ -193,21 +310,24 @@ show: $.game_versions[(@.length-1)]
```
~~~
To access multiple elements at the same time.
**Multiple properties:**
~~~markdown
```req
url: http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=rooyca&api_key=API_KEY&format=json&limit=4
url: http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=rooyca&api_key={{API_KEY}}&format=json&limit=4
show: $..recenttracks.track[0:][streamable,name,artist]
```
~~~
!!! info "Security Note"
JSONPath expressions are validated to prevent script injection. Malicious expressions will be blocked.
---
### req-uuid
Specifies the unique identifier of the request. This is useful when we want to store the response in `localStorage` and use it in other blocks or notes.
Specifies the unique identifier for caching the request response in `localStorage`.
**Example:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/users/1
@ -218,7 +338,9 @@ req-uuid: test-{{this.username}}
!!! note "Where `{{this.username}}` is a variable (`username`) defined in the frontmatter."
Stored responses can be accessed using the `req-uuid` (which won't trigger a new request).
**Accessing cached responses:**
Stored responses can be accessed using the `req-uuid` (which won't trigger a new request):
~~~markdown
```req
@ -227,7 +349,9 @@ req-uuid: name
```
~~~
Responses can also be accessed using [dataview](https://blacksmithgu.github.io/dataview/).
**Using with Dataview:**
Responses can also be accessed using [dataview](https://blacksmithgu.github.io/obsidian-dataview/):
~~~markdown
```dataviewjs
@ -235,22 +359,31 @@ dv.paragraph(localStorage.getItem("req-UUID"))
```
~~~
!!! info "Is mandatory to use `req-` before whatever you defined in `req-uuid` flag."
!!! info "Always use `req-` prefix when accessing via localStorage"
When accessing directly through localStorage, use: `localStorage.getItem("req-UUID")`
To remove responses from localStorage, run:
**Removing cached responses:**
From code:
~~~markdown
```dataviewjs
localStorage.removeItem("req-name")
```
~~~
To remove responses, go to settings and click over the response you want to delete.
From settings:
Go to Settings → APIRequest → Saved API Requests and click on the response you want to delete.
!!! tip "Cache Management"
Regularly review and clear old cached responses to free up localStorage space.
---
### hidden
Executes the code block without displaying its output.
Executes the code block without displaying its output. Useful for background requests that cache data.
**Example:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/users/1
@ -259,10 +392,16 @@ hidden
```
~~~
!!! tip "Use Case"
Great for fetching authentication tokens or other data you want to cache but not display.
---
### disabled
Disables the request. The codeblock won't be executed.
Disables the request. The codeblock won't be executed. Useful for temporarily disabling requests without deleting them.
**Example:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/users/1
@ -272,10 +411,16 @@ disabled
```
~~~
!!! tip "Development Tip"
Use `disabled` during development to prevent hitting API rate limits.
---
### save-as
Specifies the path to save the response. It'll save the entire response. A file extension is required. It won't create directories.
Saves the entire response to a file. A file extension is required.
**Example:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/posts/1
@ -283,10 +428,23 @@ save-as: posts/1.json
```
~~~
!!! warning "Important"
- File paths are validated for security
- Directory traversal (`..`) is blocked
- Absolute paths are not allowed
- The file path is relative to your vault root
- Directories are NOT created automatically
!!! tip "File Management"
Create the target directory in your vault before using `save-as`.
---
### auto-update
If present, the codeblock will automatically update the response every time is possible. This is only needed when using the flag `req-uuid`, because the default behavior of the codeblock is to run every time the note is loaded.
Forces the codeblock to fetch fresh data every time, ignoring cached responses.
**Example:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/posts/1
@ -296,10 +454,20 @@ save-as: posts/1.json
```
~~~
!!! note "When to use"
Use `auto-update` when you need real-time data or when the cached response becomes stale.
**Without `req-uuid`:** Requests always run fresh by default.
**With `req-uuid`:** Without `auto-update`, cached responses are used.
**With `auto-update`:** Ignores cache and always fetches fresh data.
---
### format
Specifies the format in which the response should be displayed. It can be any string (including `html`). If more than one output is specified, more then one format should be specified, otherwise it'd just render the first output.
Specifies a custom format for displaying the response. Supports HTML and uses `{}` as placeholders.
**Example:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/posts/1
@ -308,14 +476,40 @@ format: <h1>{}</h1> <p>{}</p>
```
~~~
!!! note "In this example, first `{}` will be replaced with the *title*, and second `{}` will be replaced with the *body*."
!!! note "In this example, the first `{}` will be replaced with the *title*, and the second `{}` will be replaced with the *body*."
**Advanced formatting:**
~~~markdown
```req
url: https://api.example.com/user/1
show: $.[name,email,age]
format: **Name:** {} | **Email:** {} | **Age:** {}
```
~~~
**List formatting:**
~~~markdown
```req
url: https://api.todoist.com/rest/v2/tasks
headers: {"Authorization": "Bearer {{TODOIST_TOKEN}}"}
show: $..content
format: - [ ] {}
```
~~~
!!! danger "Security Warning"
Format strings are sanitized to prevent XSS attacks. Script tags, event handlers, and javascript: URIs are automatically removed.
---
### properties
!!! warning "To use this flag you need a JSON response and the `show` flag"
Updates frontmatter properties with response data. Requires a JSON response and the `show` flag.
Specifies the frontmatter properties to update with the response. The data should be strings separated by commas. To set internal links use the this `[[..]]` syntax.
**Syntax:**
The data should be property names separated by commas.
**Example:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/posts/1
@ -323,3 +517,99 @@ show: $.[id,title]
properties: id, title
```
~~~
**For internal links, use `[[..]]` syntax:**
~~~markdown
```req
url: https://api.example.com/related
show: $.[id,name]
properties: relatedId, [[relatedNote]]
```
~~~
This will create:
```yaml
---
relatedId: 123
relatedNote: "[[Note Name]]"
---
```
!!! tip "Use Case"
Great for automatically populating metadata from API responses.
---
## 💡 Common Patterns
### Authentication Flow
1. Get auth token (hidden):
~~~markdown
```req
url: https://api.example.com/auth/login
method: post
body: {"username": "{{this.username}}", "password": "{{this.password}}"}
req-uuid: auth
hidden
```
~~~
2. Use token in subsequent requests:
~~~markdown
```req
url: https://api.example.com/user/data
headers: {"Authorization": "Bearer {{ls.auth>$.token}}"}
```
~~~
### Error Handling
If a request fails, an error message will be displayed:
```
Error: Failed to fetch
```
Common errors:
- **Network Error**: Check your internet connection
- **401 Unauthorized**: Check your API key/token
- **404 Not Found**: Verify the URL is correct
- **Invalid JSONPath**: Check your `show:` expression
---
## 📚 Further Reading
- [JSONPath Syntax Examples](https://github.com/JSONPath-Plus/JSONPath?tab=readme-ov-file#syntax-through-examples)
- [JSONPath Online Tester](https://jsonpath-plus.github.io/JSONPath/demo/)
- [Use Cases](usecase/index.md) - Real-world examples
---
## ❓ Troubleshooting
**Request not working?**
- Check the console (Ctrl+Shift+I) for detailed error messages
- Verify your URL is correct and uses https://
- Check that all required flags are present
- Ensure your JSONPath expression is valid
**Cached data not updating?**
- Remove `req-uuid` to always fetch fresh data
- Or add `auto-update` flag to force refreshing
- Or manually clear cache in Settings
**Variables not resolving?**
- Check frontmatter syntax is correct
- Verify global variables are defined in settings
- Ensure you're using the correct variable syntax
---
!!! tip "Need Help?"
If you encounter issues or have questions, please open an issue on [GitHub](https://github.com/Rooyca/obsidian-api-request/issues).

View file

@ -1,14 +1,34 @@
# 🔎 Overview
APIRequest (APIR) is a plugin for the note taking app [Obsidian](https://obsidian.md/) that allows you to make requests to apis display the response in your notes.
APIRequest (APIR) is a plugin for the note taking app [Obsidian](https://obsidian.md/) that allows you to make requests to APIs and display the response directly in your notes.
## 🔥 Features
- Perform requests using various methods such as `GET`, `POST`, `PUT`, and `DELETE`.
- Utilize variables from the `front-matter`, global variables or even reuse responses from another codeblocks.
- Save responses in the `localStorage` for convenient access and reuse.
- Disable code blocks as needed to optimize performance.
- Display specific values from responses, providing granular control over the presentation of data.
- **Multiple HTTP Methods**: Perform requests using `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, and `OPTIONS`.
- **Variable Substitution**: Utilize variables from the `front-matter`, global variables, or even reuse responses from other code blocks.
- **Response Caching**: Save responses in `localStorage` for convenient access and reuse across notes.
- **Performance Control**: Disable code blocks as needed to optimize performance.
- **Precise Data Extraction**: Display specific values from responses using JSONPath, providing granular control over data presentation.
- **Security First**: Built-in input validation and sanitization to protect against XSS, injection attacks, and directory traversal.
- **Auto-update**: Automatically refresh cached responses when needed.
- **Format Output**: Custom HTML/text formatting for response data.
## 🔒 Security Features
APIRequest implements comprehensive security measures:
- ✅ **URL Validation**: Only HTTPS and HTTP protocols are allowed
- ✅ **Input Sanitization**: All user inputs are validated and sanitized
- ✅ **XSS Prevention**: HTML output is sanitized to prevent script injection
- ✅ **Path Traversal Protection**: File paths are validated to prevent unauthorized access
- ✅ **Safe JSONPath**: JSONPath expressions are validated before execution
- ✅ **UUID Sanitization**: Request identifiers are sanitized to prevent injection attacks
!!! warning "Security Best Practices"
- Always use HTTPS URLs when making API requests
- Store API keys in global variables (Settings → APIRequest → Global variables), never in notes
- Only connect to trusted API endpoints
- Regularly review and clear cached responses
## ⚡ How to use
@ -28,3 +48,41 @@ disabled
```
~~~
## 📚 Quick Start
1. **Simple GET Request**
~~~markdown
```req
url: https://api.github.com/users/octocat
show: $.name
```
~~~
2. **Using Variables**
~~~markdown
```req
url: https://api.example.com/user/{{this.userId}}
headers: {"Authorization": "Bearer {{API_TOKEN}}"}
show: $.data.name
```
~~~
3. **Caching Responses**
~~~markdown
```req
url: https://api.example.com/data
req-uuid: mydata
show: $.result
```
~~~
4. **Reusing Cached Data**
~~~markdown
```req
url: https://api.example.com/more-data
headers: {"X-Token": "{{ls.mydata>$.token}}"}
```
~~~
For detailed documentation on all flags and features, see [Codeblocks](codeblocks.md).

View file

@ -1,45 +1,181 @@
# 👨🏻‍💻 Bloque de código
# 👨🏻‍💻 Bloques de Código
El `bloque de código` es un bloque versátil que se puede usar para escribir código en diferentes lenguajes. En este caso, lo usaremos para realizar solicitudes.
El `bloque de código` es un bloque versátil que se puede utilizar para realizar solicitudes API directamente desde tus notas. Esta guía cubre todas las banderas disponibles y cómo usarlas de manera efectiva y segura.
## 🏳️ Banderas
Las banderas son la forma de especificar los parámetros de nuestra solicitud y también el formato en el que queremos nuestra respuesta.
Las banderas son la forma de especificar los parámetros de nuestra solicitud. Todas las banderas **no distinguen entre mayúsculas y minúsculas**.
| Bandera | Valor predeterminado |
| ------------| ---------|
| [url](#url) | |
| [method](#method) | GET |
| [body](#body) | |
| [headers](#headers) | |
| [show](#show) | ALL |
| [req-uuid](#req-uuid) | |
| [disabled](#disabled) | |
| [save-as](#save-as) | |
| Bandera | Por defecto | Requerido | Descripción |
| ------------| ---------| -------- | ----------- |
| [url](#url) | | ✅ Sí | El endpoint de la API a solicitar |
| [method](#method) | GET | No | Método HTTP (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS) |
| [body](#body) | | No | Cuerpo de la solicitud (formato JSON) |
| [headers](#headers) | | No | Encabezados de la solicitud (formato JSON) |
| [show](#show) | ALL | No | JSONPath para extraer datos específicos |
| [req-uuid](#req-uuid) | req-general | No | ID único para almacenamiento en caché |
| [hidden](#hidden) | FALSE | No | Ocultar la salida |
| [disabled](#disabled) | | No | Deshabilitar la solicitud |
| [save-as](#save-as) | | No | Guardar la respuesta en un archivo |
| [auto-update](#auto-update) | FALSE | No | Siempre obtener datos frescos |
| [format](#format) | | No | Formato de salida personalizado |
| [properties](#properties) | | No | Actualizar propiedades del frontmatter |
### url
---
Es la única bandera **obligatoria**. Especifica la URL de la solicitud. Se pueden utilizar variables definidas en el `frontmatter`.
## 🔒 Consideraciones de Seguridad
!!! danger "Directrices de Seguridad Importantes"
- **Siempre usa HTTPS**: Usa URLs `https://` para asegurar comunicación cifrada
- **Protege las Claves API**: Almacena tokens sensibles en variables globales, nunca los codifiques directamente en las notas
- **Valida las Entradas**: El plugin valida todas las entradas, pero solo conéctate a APIs de confianza
- **Revisa los Datos en Caché**: Limpia regularmente las respuestas antiguas en caché desde la Configuración
### Validación de Entradas
Todas las entradas del usuario se validan automáticamente:
- **URLs**: Solo se permiten los protocolos `http://` y `https://`
- **UUIDs**: Sanitizados solo a alfanuméricos, guiones y guiones bajos
- **Rutas de Archivos**: Se bloquean el recorrido de directorios (`..`) y las rutas absolutas
- **JSONPath**: Las expresiones se validan para prevenir inyección de scripts
- **Cadenas de Formato**: El HTML se sanitiza para prevenir ataques XSS
---
## 📖 Variables y Reutilización de Datos
### LocalStorage y Variables
Las respuestas de la API se pueden almacenar en `localStorage` y reutilizar en otros bloques de código o notas. Para almacenar una respuesta, debes asignarle un identificador único usando la bandera `req-uuid`.
Puedes acceder a las respuestas almacenadas usando la siguiente sintaxis:
```
{{ls.UUID>JSONPath}}
```
* `UUID`: El identificador único definido en la bandera `req-uuid` (sin el prefijo `req-`)
* `JSONPath`: La ruta a los datos específicos que deseas de la respuesta
**Ejemplo:**
Si tienes una solicitud con `req-uuid: user`, puedes acceder al nombre del usuario así:
```
{{ls.user>$.name}}
```
!!! info "Nota de Seguridad"
Los UUIDs se sanitizan automáticamente. Solo se permiten caracteres alfanuméricos, guiones y guiones bajos.
---
### Variables del Frontmatter
Puedes hacer referencia a variables definidas en el **frontmatter** de la nota usando:
```
{{this.variableName}}
```
**Ejemplo:**
Frontmatter:
```yaml
---
userId: 12345
title: "Mi Nota"
---
```
Uso en la solicitud:
~~~markdown
```req
url: https://api.example.com/user/{{this.userId}}
```
~~~
!!! warning "Advertencia de Seguridad"
Evita almacenar claves API sensibles en el frontmatter. Usa variables globales en su lugar.
---
### Variables Globales
Define variables reutilizables en la configuración del plugin (Configuración → APIRequest → Variables globales). Estas se almacenan de forma segura y se pueden acceder con:
```
{{VARIABLE_NAME}}
```
**Ejemplo:**
1. En la configuración del plugin, agrega:
- Clave: `API_KEY`
- Valor: `your-secret-key`
2. En tu solicitud:
~~~markdown
```req
# un comentario
url: https://api.example.com/data
headers: {"Authorization": "Bearer {{API_KEY}}"}
```
~~~
!!! tip "Mejor Práctica"
Almacena todas las claves API y tokens como variables globales para mantener tus notas seguras y portables.
---
### Variables Especiales
- `{{this.file.name}}` - Nombre base del archivo actual (sin extensión)
---
## Detalles de las Banderas
### url
La única bandera **requerida**. Especifica el endpoint de la solicitud API.
**Sintaxis:**
~~~markdown
```req
url: https://api.example.com/endpoint
```
~~~
**Con variables:**
~~~markdown
```req
# esto es solo un comentario
url: https://jsonplaceholder.typicode.com/users/{{this.id}}
```
~~~
!!! note "Donde `{{this.id}}` es una variable (`id`) definida en el frontmatter."
!!! danger "Seguridad"
Solo se permiten URLs HTTPS y HTTP. Otros protocolos (file://, javascript:, etc.) están bloqueados por seguridad.
---
### method
Especifica el método de solicitud. El valor predeterminado es `GET` y los valores disponibles son:
Especifica el método de solicitud HTTP. El valor por defecto es `GET`.
- GET
**Métodos disponibles:**
- GET (por defecto)
- POST
- PUT
- DELETE
- PATCH
- HEAD
- OPTIONS
**Ejemplo:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/posts
@ -47,24 +183,39 @@ method: post
```
~~~
---
### body
Especifica el cuerpo de la solicitud. El valor predeterminado es un objeto vacío. Los datos deben estar en formato JSON separando las claves y valores con dos puntos y espacio. Se pueden utilizar variables definidas en el `frontmatter`.
Especifica el cuerpo de la solicitud. Los datos deben estar en formato JSON.
**Ejemplo:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/posts
method: post
body: {"title": {{this.title}}, "body": "bar", "userId": 1}
body: {"title": "{{this.filename}}", "body": "bar", "userId": 1}
```
~~~
!!! note "Donde `{{this.title}}` es una variable (`title`) definida en el frontmatter."
!!! note "Donde `{{this.filename}}` es el nombre del archivo de trabajo."
**Uso avanzado con variables:**
~~~markdown
```req
url: https://api.example.com/create
method: post
body: {"userId": {{this.userId}}, "token": "{{API_TOKEN}}"}
```
~~~
---
### headers
Especifica los encabezados de la solicitud. El valor predeterminado es un objeto vacío. Los datos deben estar en formato JSON separando las claves y valores con dos puntos y espacio. Se pueden utilizar variables definidas en el `frontmatter`.
Especifica los encabezados de la solicitud. Los datos deben estar en formato JSON.
**Ejemplo:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/posts
@ -73,10 +224,32 @@ headers: {"Content-type": "application/json; charset=UTF-8"}
```
~~~
**Usando respuestas en caché como encabezados:**
Puedes usar respuestas de otras solicitudes como encabezados/cuerpo/url/show. Por ejemplo, si tienes una solicitud con `req-uuid: token`, puedes usarla así:
~~~markdown
```req
url: https://api.todoist.com/rest/v2/tasks
headers: {"Authorization": "Bearer {{ls.token>$.access_token}}"}
show: $..content
format: - [ ] {}
req-uuid: todos
```
~~~
!!! tip "Encabezados Comunes"
- `Content-Type: application/json` - Para datos JSON
- `Authorization: Bearer TOKEN` - Para autenticación API
- `Accept: application/json` - Para solicitar respuestas JSON
---
### show
Especifica los datos de respuesta que se van a mostrar. Ver [ejemplos de JSONPath](https://github.com/JSONPath-Plus/JSONPath?tab=readme-ov-file#syntax-through-examples), o prueba la herramienta online de [jsonpath-plus](https://jsonpath-plus.github.io/JSONPath/demo/).
Especifica qué datos de la respuesta mostrar usando expresiones JSONPath. Ver [ejemplos de JSONPath](https://github.com/JSONPath-Plus/JSONPath?tab=readme-ov-file#syntax-through-examples), o prueba la herramienta en línea de [jsonpath-plus](https://jsonpath-plus.github.io/JSONPath/demo/).
**Ejemplo simple:**
~~~markdown
```req
url: https://api.chess.com/pub/player/hikaru/stats
@ -84,8 +257,7 @@ show: $['chess_daily']['last']['rating']
```
~~~
Se pueden mostrar múltiples resultados usando `[]`.
**Múltiples salidas usando corchetes:**
~~~markdown
```req
url: https://api.chess.com/pub/player/hikaru/stats
@ -93,8 +265,17 @@ show: $.chess_daily[last,best].rating
```
~~~
También es posible iterar sobre un arreglo. El siguiente ejemplo muestra la ciudad de todos los usuarios.
**Múltiples salidas usando +:**
~~~markdown
```req
url: https://api.chess.com/pub/player/hikaru/stats
show: $.chess_daily[last,best].rating + $.chess960_daily[last,best].rating
```
~~~
**Iterando sobre arrays:**
Obtener ciudad de todos los usuarios:
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/users
@ -102,8 +283,7 @@ show: $..address.city
```
~~~
También es posible iterar sobre un número especificado de elementos del arreglo.
**Iterando sobre los primeros N elementos:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/users
@ -111,8 +291,7 @@ show: $..[:3].address.city
```
~~~
También es posible iterar sobre un rango especificado de índices del arreglo.
**Índices específicos del array:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/users
@ -120,8 +299,9 @@ show: $..[3,2,6].address.city
```
~~~
Puedes acceder al último elemento usando `(@.length-1)` o simplemente `[-1:]`.
**Último elemento:**
Puedes acceder al último elemento usando `(@.length-1)` o simplemente `[-1:]`:
~~~markdown
```req
url: https://api.modrinth.com/v2/project/distanthorizons
@ -129,62 +309,98 @@ show: $.game_versions[(@.length-1)]
```
~~~
Para acceder a multiples resultados podemos usar:
**Múltiples propiedades:**
~~~markdown
```req
url: http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=rooyca&api_key=API_KEY&format=json&limit=4
url: http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=rooyca&api_key={{API_KEY}}&format=json&limit=4
show: $..recenttracks.track[0:][streamable,name,artist]
```
~~~
!!! info "Nota de Seguridad"
Las expresiones JSONPath se validan para prevenir inyección de scripts. Las expresiones maliciosas serán bloqueadas.
---
### req-uuid
Especifica el ID con la que se almacenará la solicitud. Esto es útil cuando queremos almacenar la respuesta en `localStorage` y usarla en otros bloques o notas.
Especifica el identificador único para almacenar en caché la respuesta de la solicitud en `localStorage`.
**Ejemplo:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/users/1
show: $.name
req-uuid: name
req-uuid: test-{{this.username}}
```
~~~
Las respuestas almacenadas se pueden ver usando el `req-uuid` con la bandera `disabled` (que no activará una nueva solicitud).
!!! note "Donde `{{this.username}}` es una variable (`username`) definida en el frontmatter."
**Accediendo a respuestas en caché:**
Las respuestas almacenadas se pueden acceder usando el `req-uuid` (lo cual no activará una nueva solicitud):
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/users/1
req-uuid: name
disabled
```
~~~
Las respuestas también se pueden ver usando [dataview](https://blacksmithgu.github.io/dataview/).
**Usando con Dataview:**
Las respuestas también se pueden acceder usando [dataview](https://blacksmithgu.github.io/obsidian-dataview/):
~~~markdown
```dataview
dv.paragraph(localStorage.getItem("req-name"))
```dataviewjs
dv.paragraph(localStorage.getItem("req-UUID"))
```
~~~
!!! info "Es obligatorio usar `req-` antes de lo que sea que hayas definido en la bandera `req-uuid`."
!!! info "Siempre usa el prefijo `req-` cuando accedas vía localStorage"
Al acceder directamente a través de localStorage, usa: `localStorage.getItem("req-UUID")`
Para eliminar respuestas de localStorage, ejecuta:
**Eliminando respuestas en caché:**
Desde código:
~~~markdown
```dataview
```dataviewjs
localStorage.removeItem("req-name")
```
~~~
Para eliminar todas las respuestas, ve a configuraciones y haz clic sobre la respuesta que quieras eliminar.
Desde configuración:
Ve a Configuración → APIRequest → Solicitudes API Guardadas y haz clic en la respuesta que deseas eliminar.
!!! tip "Gestión de Caché"
Revisa y limpia regularmente las respuestas antiguas en caché para liberar espacio en localStorage.
---
### hidden
Ejecuta el bloque de código sin mostrar su salida. Útil para solicitudes en segundo plano que almacenan datos en caché.
**Ejemplo:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/users/1
req-uuid: name
hidden
```
~~~
!!! tip "Caso de Uso"
Excelente para obtener tokens de autenticación u otros datos que deseas almacenar en caché pero no mostrar.
---
### disabled
Deshabilita la solicitud. El codeblock no se ejecutará.
Deshabilita la solicitud. El bloque de código no se ejecutará. Útil para deshabilitar temporalmente solicitudes sin eliminarlas.
**Ejemplo:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/users/1
@ -194,10 +410,16 @@ disabled
```
~~~
!!! tip "Consejo de Desarrollo"
Usa `disabled` durante el desarrollo para evitar alcanzar los límites de tasa de la API.
---
### save-as
Especifica la ruta para guardar la respuesta. Guardará toda la respuesta. Se requiere una extensión de archivo. No creará directorios.
Guarda la respuesta completa en un archivo. Se requiere una extensión de archivo.
**Ejemplo:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/posts/1
@ -205,10 +427,23 @@ save-as: posts/1.json
```
~~~
!!! warning "Importante"
- Las rutas de archivo se validan por seguridad
- Se bloquea el recorrido de directorios (`..`)
- No se permiten rutas absolutas
- La ruta del archivo es relativa a la raíz de tu bóveda
- Los directorios NO se crean automáticamente
!!! tip "Gestión de Archivos"
Crea el directorio de destino en tu bóveda antes de usar `save-as`.
---
### auto-update
El codeblock se actualizará de manera automatica cada que sea posible. Esto solo es necesario cuando la bandera `req-uuid` está precente, porque el comportamiento predeterminado del codeblock es ejecutarse cada vez que se carga la nota.
Fuerza al bloque de código a obtener datos frescos cada vez, ignorando las respuestas en caché.
**Ejemplo:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/posts/1
@ -216,4 +451,171 @@ req-uuid: firstPost
auto-update
save-as: posts/1.json
```
~~~
~~~
!!! note "Cuándo usar"
Usa `auto-update` cuando necesites datos en tiempo real o cuando la respuesta en caché se vuelva obsoleta.
**Sin `req-uuid`:** Las solicitudes siempre se ejecutan frescas por defecto.
**Con `req-uuid`:** Sin `auto-update`, se usan las respuestas en caché.
**Con `auto-update`:** Ignora el caché y siempre obtiene datos frescos.
---
### format
Especifica un formato personalizado para mostrar la respuesta. Admite HTML y usa `{}` como marcadores de posición.
**Ejemplo:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/posts/1
show: $.[title,body]
format: <h1>{}</h1> <p>{}</p>
```
~~~
!!! note "En este ejemplo, el primer `{}` será reemplazado con el *título*, y el segundo `{}` será reemplazado con el *cuerpo*."
**Formato avanzado:**
~~~markdown
```req
url: https://api.example.com/user/1
show: $.[name,email,age]
format: **Nombre:** {} | **Email:** {} | **Edad:** {}
```
~~~
**Formato de lista:**
~~~markdown
```req
url: https://api.todoist.com/rest/v2/tasks
headers: {"Authorization": "Bearer {{TODOIST_TOKEN}}"}
show: $..content
format: - [ ] {}
```
~~~
!!! danger "Advertencia de Seguridad"
Las cadenas de formato se sanitizan para prevenir ataques XSS. Las etiquetas de script, manejadores de eventos y URIs javascript: se eliminan automáticamente.
---
### properties
Actualiza las propiedades del frontmatter con datos de la respuesta. Requiere una respuesta JSON y la bandera `show`.
**Sintaxis:**
Los datos deben ser nombres de propiedades separados por comas.
**Ejemplo:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/posts/1
show: $.[id,title]
properties: id, title
```
~~~
**Para enlaces internos, usa la sintaxis `[[..]]`:**
~~~markdown
```req
url: https://api.example.com/related
show: $.[id,name]
properties: relatedId, [[relatedNote]]
```
~~~
Esto creará:
```yaml
---
relatedId: 123
relatedNote: "[[Note Name]]"
---
```
!!! tip "Caso de Uso"
Excelente para poblar automáticamente metadatos desde respuestas API.
---
## 💡 Patrones Comunes
### Flujo de Autenticación
1. Obtener token de autenticación (oculto):
~~~markdown
```req
url: https://api.example.com/auth/login
method: post
body: {"username": "{{this.username}}", "password": "{{this.password}}"}
req-uuid: auth
hidden
```
~~~
2. Usar el token en solicitudes subsecuentes:
~~~markdown
```req
url: https://api.example.com/user/data
headers: {"Authorization": "Bearer {{ls.auth>$.token}}"}
```
~~~
### Manejo de Errores
Si una solicitud falla, se mostrará un mensaje de error:
```
Error: Failed to fetch
```
Errores comunes:
- **Error de Red**: Verifica tu conexión a internet
- **401 No Autorizado**: Verifica tu clave/token API
- **404 No Encontrado**: Verifica que la URL sea correcta
- **JSONPath Inválido**: Verifica tu expresión `show:`
### Consejos de Rendimiento
1. **Almacena en caché las respuestas** - Usa `req-uuid` para datos que no cambian a menudo
2. **Usa `disabled`** - Deshabilita temporalmente solicitudes costosas durante el desarrollo
3. **Usa `hidden`** - Oculta la salida para la obtención de datos en segundo plano
---
## 📚 Lecturas Adicionales
- [Ejemplos de Sintaxis JSONPath](https://github.com/JSONPath-Plus/JSONPath?tab=readme-ov-file#syntax-through-examples)
- [Probador en Línea de JSONPath](https://jsonpath-plus.github.io/JSONPath/demo/)
- [Casos de Uso](usecase/index.md) - Ejemplos del mundo real
---
## ❓ Solución de Problemas
**¿La solicitud no funciona?**
- Verifica la consola (Ctrl+Shift+I) para mensajes de error detallados
- Verifica que tu URL sea correcta y use https://
- Verifica que todas las banderas requeridas estén presentes
- Asegúrate de que tu expresión JSONPath sea válida
**¿Los datos en caché no se actualizan?**
- Elimina `req-uuid` para siempre obtener datos frescos
- O agrega la bandera `auto-update` para forzar la actualización
- O limpia manualmente el caché en Configuración
**¿Las variables no se resuelven?**
- Verifica que la sintaxis del frontmatter sea correcta
- Verifica que las variables globales estén definidas en la configuración
- Asegúrate de estar usando la sintaxis correcta de variables
---
!!! tip "¿Necesitas Ayuda?"
Si encuentras problemas o tienes preguntas, por favor abre un issue en [GitHub](https://github.com/Rooyca/obsidian-api-request/issues).

View file

@ -1,14 +1,34 @@
# 🔎 APIRequest
# 🔎 Descripción General
APIRequest (APIR) es un plugin para [Obsidian](https://obsidian.md/) que te permite realizar solicitudes HTTP y mostrar la respuesta en tus notas.
APIRequest (APIR) es un plugin para la aplicación de toma de notas [Obsidian](https://obsidian.md/) que te permite realizar solicitudes a APIs y mostrar la respuesta directamente en tus notas.
## 🔥 Características
- Realiza solicitudes HTTP utilizando varios métodos como `GET`, `POST`, `PUT` y `DELETE`.
- Utiliza variables del front-matter dentro de bloques de código.
- Guarda respuestas en `localStorage` para un acceso y reutilización convenientes.
- Desactiva bloques de código según sea necesario para optimizar el rendimiento.
- Muestra valores específicos de las respuestas, proporcionando un control detallado sobre la presentación de datos.
- **Múltiples Métodos HTTP**: Realiza solicitudes usando `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD` y `OPTIONS`.
- **Sustitución de Variables**: Utiliza variables del `front-matter`, variables globales o incluso reutiliza respuestas de otros bloques de código.
- **Caché de Respuestas**: Guarda respuestas en `localStorage` para acceso y reutilización conveniente entre notas.
- **Control de Rendimiento**: Desactiva bloques de código según sea necesario para optimizar el rendimiento.
- **Extracción Precisa de Datos**: Muestra valores específicos de las respuestas usando JSONPath, proporcionando control granular sobre la presentación de datos.
- **Seguridad Primero**: Validación y sanitización de entradas incorporada para proteger contra XSS, ataques de inyección y directory traversal.
- **Auto-actualización**: Actualiza automáticamente respuestas en caché cuando sea necesario.
- **Formato de Salida**: Formato HTML/texto personalizado para datos de respuesta.
## 🔒 Características de Seguridad
APIRequest implementa medidas de seguridad completas:
- ✅ **Validación de URL**: Solo se permiten protocolos HTTPS y HTTP
- ✅ **Sanitización de Entradas**: Todas las entradas de usuario son validadas y sanitizadas
- ✅ **Prevención de XSS**: La salida HTML es sanitizada para prevenir inyección de scripts
- ✅ **Protección contra Directory Traversal**: Las rutas de archivos son validadas para prevenir acceso no autorizado
- ✅ **JSONPath Seguro**: Las expresiones JSONPath son validadas antes de la ejecución
- ✅ **Sanitización de UUID**: Los identificadores de solicitud son sanitizados para prevenir ataques de inyección
!!! warning "Mejores Prácticas de Seguridad"
- Siempre usa URLs HTTPS al realizar solicitudes API
- Almacena claves API en variables globales (Configuración → APIRequest → Variables globales), nunca en notas
- Revisa y limpia regularmente respuestas en caché (Configuración → APIRequest → Solicitudes API Guardadas)
- Solo conéctate a endpoints API de confianza
## ⚡ Cómo usar
@ -23,7 +43,49 @@ method: post
body: {"id":1}
headers: {"Accept": "application/json"}
show: $.id
req-uuid: id-persona
req-uuid: IDpersona
disabled
```
~~~
~~~
## 📚 Inicio Rápido
1. **Solicitud GET Simple**
~~~markdown
```req
url: https://api.github.com/users/octocat
show: $.name
```
~~~
2. **Usando Variables**
~~~markdown
```req
url: https://api.example.com/user/{{this.userId}}
headers: {"Authorization": "Bearer {{API_TOKEN}}"}
show: $.data.name
```
~~~
3. **Almacenando Respuestas en Caché**
~~~markdown
```req
url: https://api.example.com/data
req-uuid: mydata
show: $.result
```
~~~
4. **Reutilizando Datos en Caché**
~~~markdown
```req
url: https://api.example.com/more-data
headers: {"X-Token": "{{ls.mydata>$.token}}"}
```
~~~
Para documentación detallada sobre todas las banderas y características, consulta [Bloques de código](codeblocks.md).

View file

@ -1,9 +1,34 @@
# Ejemplos de uso
# Casos de uso
A collection of use cases for this plugin. **If you have a one, please share it with us.**
Una colección ejemplos de uso para este plugin. **Si tienes uno, por favor compártelo con nosotros.**
Una colección de casos de uso para este complemento. **Si tienes uno, por favor compártelo con nosotros.**
## Ver el precio de BITCOIN (o cualquier criptomoneda)
## Uso de respuestas en línea con [Dataview](https://blacksmithgu.github.io/obsidian-dataview/)
Primero, haz una solicitud y almacena la respuesta usando `req-uuid`:
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/comments/1
req-uuid: test
hidden
```
~~~
A continuación, con **DataviewJS** y las consultas JavaScript en línea habilitadas, puedes acceder a los datos guardados de esta manera:
```markdown
El correo electrónico es `$=dv.el("span", JSON.parse(localStorage.getItem("req-test")).email)`
y el ID es `$=dv.el("span", JSON.parse(localStorage.getItem("req-test")).id, { cls: "mod-warning" })`
```
Esto recupera el correo electrónico y el ID de la respuesta guardada (el prefijo `req-` siempre es requerido).
Aquí, también estamos añadiendo una clase personalizada a la segunda consulta en línea.
La salida renderizada se verá así:
> El correo electrónico es [Eliseo@gardner.biz](mailto:Eliseo@gardner.biz) y el ID es <span style="color:red;">1</span>
## Consultar el precio de BITCOIN (o cualquier criptomoneda)
~~~makdown
```req
@ -23,7 +48,7 @@ show: $.main.temp
```
~~~
## Buscar peliculas
## Buscar películas
~~~makdown
```req
@ -32,25 +57,27 @@ show: $.results[0:].title
```
~~~
!!! info "Nota el uso de `{{this.title}}`. Esta es una característica que te permite pasar propiedades del front-matter."
!!! info "Observa el uso de `{{this.title}}`. Esta es una característica que te permite pasar propiedades del front-matter."
## Obtener más de un resultado
## Renderizar datos
~~~makdown
~~~markdown
```req
url: https://mapi.mobilelegends.com/hero/detail?id={{this.file.name}}
url: https://mapi.mobilelegends.com/hero/detail?id=1
show: $.data[cover_picture,name,type]
format: ![img]({}) <br> <strong>Name:</strong> {} <br> <strong>Type:</strong> {}
```
~~~
## Obtener Tareas de [todoist](https://todoist.com/)
## Obtener TAREAS desde [todoist](https://todoist.com/)
~~~makdown
```req
url: https://api.todoist.com/rest/v2/tasks
headers: {"Authorization": "Bearer YOUR_TOKEN"}
show: $..content
req-uuid: todos
format: - [ ] {}
req-id: todos
```
~~~
@ -58,4 +85,4 @@ req-uuid: todos
## Tu caso de uso
> **Si deseas compartir tu caso de uso, por favor siéntete libre de abrir una PR o un [Issue](https://github.com/Rooyca/obsidian-api-request/issues/new/choose)**
> **Si quieres compartir tu caso de uso, siéntete libre de abrir un PR o un [Issue](https://github.com/Rooyca/obsidian-api-request/issues/new/choose).**

View file

@ -1,43 +1,181 @@
# 👨🏻‍💻 代码块
`codeblock` 是一个多功能块,可用于用不同语言编写代码。在本例中,我们将使用它来发出请求
`codeblock` 是一个多功能代码块,可用于直接从你的笔记中发送 API 请求。本指南涵盖所有可用的标志以及如何有效且安全地使用它们
## 🏳️ Flag
## 🏳️ 标志
Flag是指定请求参数以及我们想要的响应格式的方式
标志是指定请求参数的方式。所有标志都**不区分大小写**
| 标志 | 默认 |
| ------------| ---------|
| [url](#url) | |
| [method](#method) | GET |
| [body](#body) | |
| [headers](#headers) | |
| [show](#show) | ALL |
| [req-id](#req-id) | req-general |
| [disabled](#disabled) | |
| [save-as](#save-as) | |
| 标志 | 默认值 | 必需 | 描述 |
| ------------| ---------| -------- | ----------- |
| [url](#url) | | ✅ 是 | 要请求的 API 端点 |
| [method](#method) | GET | 否 | HTTP 方法 (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS) |
| [body](#body) | | 否 | 请求体 (JSON 格式) |
| [headers](#headers) | | 否 | 请求头 (JSON 格式) |
| [show](#show) | ALL | 否 | 用于提取特定数据的 JSONPath |
| [req-uuid](#req-uuid) | req-general | 否 | 用于缓存的唯一 ID |
| [hidden](#hidden) | FALSE | 否 | 隐藏输出 |
| [disabled](#disabled) | | 否 | 禁用请求 |
| [save-as](#save-as) | | 否 | 将响应保存到文件 |
| [auto-update](#auto-update) | FALSE | 否 | 总是获取新数据 |
| [format](#format) | | 否 | 自定义输出格式 |
| [properties](#properties) | | 否 | 更新前置元数据属性 |
---
## 🔒 安全注意事项
!!! danger "重要安全指南"
- **始终使用 HTTPS**: 使用 `https://` URL 以确保加密通信
- **保护 API 密钥**: 将敏感令牌存储在全局变量中,永远不要在笔记中硬编码
- **验证输入**: 插件会验证所有输入,但只连接到受信任的 API
- **检查缓存数据**: 定期从设置中清除旧的缓存响应
### 输入验证
所有用户输入都会自动验证:
- **URL**: 只允许 `http://``https://` 协议
- **UUID**: 仅清理为字母数字、连字符和下划线
- **文件路径**: 阻止目录遍历 (`..`) 和绝对路径
- **JSONPath**: 验证表达式以防止脚本注入
- **格式字符串**: 清理 HTML 以防止 XSS 攻击
---
## 📖 变量与数据复用
### 本地存储与变量
API 响应可以存储在 `localStorage` 中,并在其他代码块或笔记中重用。要存储响应,必须使用 `req-uuid` 标志为其分配唯一标识符。
你可以使用以下语法访问存储的响应:
```
{{ls.UUID>JSONPath}}
```
* `UUID`: 在 `req-uuid` 标志中定义的唯一标识符(不含 `req-` 前缀)
* `JSONPath`: 你想要从响应中获取的特定数据的路径
**示例:**
如果你有一个带有 `req-uuid: user` 的请求,你可以这样访问用户的名字:
```
{{ls.user>$.name}}
```
!!! info "安全说明"
UUID 会自动清理。只允许字母数字字符、连字符和下划线。
---
### 前置元数据变量
你可以使用以下语法引用笔记**前置元数据**中定义的变量:
```
{{this.variableName}}
```
**示例:**
前置元数据:
```yaml
---
userId: 12345
title: "我的笔记"
---
```
在请求中使用:
~~~markdown
```req
url: https://api.example.com/user/{{this.userId}}
```
~~~
!!! warning "安全警告"
避免在前置元数据中存储敏感的 API 密钥。请改用全局变量。
---
### 全局变量
在插件设置中定义可重用的变量(设置 → APIRequest → 全局变量)。这些变量会被安全存储,可以通过以下方式访问:
```
{{VARIABLE_NAME}}
```
**示例:**
1. 在插件设置中添加:
- 键: `API_KEY`
- 值: `your-secret-key`
2. 在你的请求中:
~~~markdown
```req
url: https://api.example.com/data
headers: {"Authorization": "Bearer {{API_KEY}}"}
```
~~~
!!! tip "最佳实践"
将所有 API 密钥和令牌存储为全局变量,以保持笔记的安全性和可移植性。
---
### 特殊变量
- `{{this.file.name}}` - 当前文件的基本名称(不含扩展名)
---
## 标志详情
### url
是唯一的**必需**标志。它指定请求的端点。可以使用 `frontmatter` 中定义的变量。
唯一**必需**标志。它指定 API 请求的端点。
**语法:**
~~~markdown
```req
url: https://api.example.com/endpoint
```
~~~
**使用变量:**
~~~markdown
```req
# 这只是一个注释
url: https://jsonplaceholder.typicode.com/users/{{this.id}}
```
~~~
!!! note "其中`{{this.id}}`是frontmatter中定义的变量`id`)。"
!!! note "其中 `{{this.id}}` 是在前置元数据中定义的变量 (`id`)。"
!!! danger "安全性"
只允许 HTTPS 和 HTTP URL。出于安全考虑,其他协议(file://、javascript: 等)会被阻止。
---
### method
指定请求方法。默认值为 `GET`,可用值为:
指定 HTTP 请求方法。默认值为 `GET`
- GET
**可用方法:**
- GET (默认)
- POST
- PUT
- DELETE
- PATCH
- HEAD
- OPTIONS
**示例:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/posts
@ -45,24 +183,39 @@ method: post
```
~~~
---
### body
指定请求的正文。默认值为空对象。数据应为 JSON 格式,双引号用冒号和空格分隔键和值。可以使用 `frontmatter` 中定义的变量
指定请求体。数据应为 JSON 格式
**示例:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/posts
method: post
body: {"title": {{this.title}}, "body": "bar", "userId": 1}
body: {"title": "{{this.filename}}", "body": "bar", "userId": 1}
```
~~~
!!! note "其中 `{{this.title}}` 是 frontmatter 中定义的变量(`title`)。"
!!! note "其中 `{{this.filename}}` 是工作文件的名称。"
**使用变量的高级用法:**
~~~markdown
```req
url: https://api.example.com/create
method: post
body: {"userId": {{this.userId}}, "token": "{{API_TOKEN}}"}
```
~~~
---
### headers
指定请求的标头。默认值为空对象。数据应为 JSON 格式,双引号将键和值用冒号和空格分隔。可以使用 `frontmatter` 中定义的变量。
指定请求头。数据应为 JSON 格式。
**示例:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/posts
@ -71,145 +224,398 @@ headers: {"Content-type": "application/json; charset=UTF-8"}
```
~~~
**使用缓存响应作为请求头:**
你可以使用其他请求的响应作为 headers/body/url/show。例如,如果你有一个带有 `req-uuid: token` 的请求,你可以这样使用:
~~~markdown
```req
url: https://api.todoist.com/rest/v2/tasks
headers: {"Authorization": "Bearer {{ls.token>$.access_token}}"}
show: $..content
format: - [ ] {}
req-uuid: todos
```
~~~
!!! tip "常用请求头"
- `Content-Type: application/json` - 用于 JSON 数据
- `Authorization: Bearer TOKEN` - 用于 API 认证
- `Accept: application/json` - 请求 JSON 响应
---
### show
指定要显示的响应数据。使用右箭头 `->` 访问嵌套对象。默认值为 `ALL`
使用 JSONPath 表达式指定要显示响应中的哪些数据。查看 [JSONPath 示例](https://github.com/JSONPath-Plus/JSONPath?tab=readme-ov-file#syntax-through-examples),或尝试 [jsonpath-plus](https://jsonpath-plus.github.io/JSONPath/demo/) 在线工具
**简单示例:**
~~~markdown
```req
url: https://api.chess.com/pub/player/hikaru/stats
show: chess_daily -> last -> rating
show: $['chess_daily']['last']['rating']
```
~~~
可以通过用逗号分隔来显示多个输出。
**使用方括号的多个输出:**
~~~markdown
```req
url: https://api.chess.com/pub/player/hikaru/stats
show: chess_daily -> last -> rating, chess_daily -> best -> rating
format: <p>Last game: {}</p> <strong>Best game: {}</strong>
render
show: $.chess_daily[last,best].rating
```
~~~
也可以使用 `{..}` 循环遍历数组。以下示例从所有用户中检索城市 (city)。
**使用 + 的多个输出:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/users
show: {..} -> address -> city
url: https://api.chess.com/pub/player/hikaru/stats
show: $.chess_daily[last,best].rating + $.chess960_daily[last,best].rating
```
~~~
也可以使用 `{n..n}` 循环遍历数组中指定数量的元素。
**循环遍历数组:**
获取所有用户的城市:
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/users
show: {0..2} -> address -> city
show: $..address.city
```
~~~
也可以使用 `{n-n-n}` 循环遍历数组的指定范围的索引。
**循环遍历前 N 个元素:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/users
show: {0-2-1} -> address -> city
show: $..[:3].address.city
```
~~~
您可以使用 `{-1}` 访问最后一个元素...
~~~markdown
```req
url:https://api.modrinth.com/v2/project/distanthorizons
show: game_versions -> {-1}
```
~~~
...或者使用 `{len}` 获取数组的长度。
~~~markdown
```req
url:https://api.modrinth.com/v2/project/distanthorizons
show: game_versions -> {len}
```
~~~
使用 `{..}` 时,若要同时访问多个元素,请使用 `&` 分隔键,并使用 `.` 访问值。
**特定数组索引:**
~~~markdown
```req
url: http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=rooyca&api_key=API_KEY&format=json&limit=4
show: recenttracks -> track -> {..} -> name & artist.#text & streamable
maketable: name, artist, stream
url: https://jsonplaceholder.typicode.com/users
show: $..[3,2,6].address.city
```
~~~
### req-id
**最后一个元素:**
指定请求的 ID。默认值为 `req-general`。当我们想要将响应存储在 `localStorage` 中并在其他块或注释中使用它时,这很有用。
你可以使用 `(@.length-1)` 或只使用 `[-1:]` 访问最后一个元素:
~~~markdown
```req
url: https://api.modrinth.com/v2/project/distanthorizons
show: $.game_versions[(@.length-1)]
```
~~~
**多个属性:**
~~~markdown
```req
url: http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=rooyca&api_key={{API_KEY}}&format=json&limit=4
show: $..recenttracks.track[0:][streamable,name,artist]
```
~~~
!!! info "安全说明"
JSONPath 表达式会被验证以防止脚本注入。恶意表达式将被阻止。
---
### req-uuid
指定用于在 `localStorage` 中缓存请求响应的唯一标识符。
**示例:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/users/1
show: $.name
req-uuid: test-{{this.username}}
```
~~~
!!! note "其中 `{{this.username}}` 是在前置元数据中定义的变量 (`username`)。"
**访问缓存响应:**
可以使用 `req-uuid` 访问存储的响应(这不会触发新请求):
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/users/1
show: name
req-id: name
req-uuid: name
```
~~~
可以使用带有 `disabled` 标志的 `req-id` 访问存储的响应(不会触发新请求)。
**与 Dataview 一起使用:**
也可以使用 [dataview](https://blacksmithgu.github.io/obsidian-dataview/) 访问响应:
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/users/1
req-id: name
disabled
```dataviewjs
dv.paragraph(localStorage.getItem("req-UUID"))
```
~~~
也可以使用 [dataview](https://blacksmithgu.github.io/dataview/) 访问响应。
!!! info "通过 localStorage 访问时始终使用 `req-` 前缀"
通过 localStorage 直接访问时,使用: `localStorage.getItem("req-UUID")`
**删除缓存响应:**
从代码中:
~~~markdown
```dataview
dv.paragraph(localStorage.getItem("req-name"))
```
~~~
!!! info "在`req-id`标志中定义的任何内容之前,必须使用`req-`"
要从 localStorage 中删除响应,请运行:
~~~markdown
```dataview
```dataviewjs
localStorage.removeItem("req-name")
```
~~~
要删除所有响应,请转到设置并单击 `Clear ID's` (清除ID) 按钮。
从设置中:
转到 设置 → APIRequest → 已保存的 API 请求,然后点击你想删除的响应。
### disabled
!!! tip "缓存管理"
定期检查并清除旧的缓存响应以释放 localStorage 空间。
禁用请求。如果指定了 `req-id`APIR 将在`localStorage`中检查响应。如果未找到它将发出新请求并存储它。之后APIR 将使用存储的响应。
---
### hidden
执行代码块但不显示其输出。适用于缓存数据的后台请求。
**示例:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/users/1
show: name
req-id: name
req-uuid: name
hidden
```
~~~
!!! tip "用例"
非常适合获取身份验证令牌或其他你想缓存但不显示的数据。
---
### disabled
禁用请求。代码块不会被执行。适用于临时禁用请求而不删除它们。
**示例:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/users/1
show: $.name
req-uuid: name
disabled
```
~~~
!!! tip "开发提示"
在开发期间使用 `disabled` 以防止达到 API 速率限制。
---
### save-as
指定保存响应的路径。它将保存整个响应。需要文件扩展名。它不会创建目录。
将整个响应保存到文件。需要文件扩展名。
**示例:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/posts/1
save-as: posts/1.json
```
~~~
!!! warning "重要"
- 文件路径会进行安全验证
- 阻止目录遍历 (`..`)
- 不允许绝对路径
- 文件路径相对于你的仓库根目录
- 不会自动创建目录
!!! tip "文件管理"
在使用 `save-as` 之前,在你的仓库中创建目标目录。
---
### auto-update
强制代码块每次都获取新数据,忽略缓存响应。
**示例:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/posts/1
req-uuid: firstPost
auto-update
save-as: posts/1.json
```
~~~
!!! note "何时使用"
当你需要实时数据或缓存响应过时时使用 `auto-update`
**不使用 `req-uuid`:** 请求默认总是获取新数据。
**使用 `req-uuid`:** 不使用 `auto-update` 时,会使用缓存响应。
**使用 `auto-update`:** 忽略缓存并总是获取新数据。
---
### format
指定显示响应的自定义格式。支持 HTML 并使用 `{}` 作为占位符。
**示例:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/posts/1
show: $.[title,body]
format: <h1>{}</h1> <p>{}</p>
```
~~~
!!! note "在此示例中,第一个 `{}` 将被替换为 *title*,第二个 `{}` 将被替换为 *body*。"
**高级格式化:**
~~~markdown
```req
url: https://api.example.com/user/1
show: $.[name,email,age]
format: **Name:** {} | **Email:** {} | **Age:** {}
```
~~~
**列表格式化:**
~~~markdown
```req
url: https://api.todoist.com/rest/v2/tasks
headers: {"Authorization": "Bearer {{TODOIST_TOKEN}}"}
show: $..content
format: - [ ] {}
```
~~~
!!! danger "安全警告"
格式字符串会被清理以防止 XSS 攻击。脚本标签、事件处理程序和 javascript: URI 会被自动删除。
---
### properties
使用响应数据更新前置元数据属性。需要 JSON 响应和 `show` 标志。
**语法:**
数据应为用逗号分隔的属性名称。
**示例:**
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/posts/1
show: $.[id,title]
properties: id, title
```
~~~
**对于内部链接,使用 `[[..]]` 语法:**
~~~markdown
```req
url: https://api.example.com/related
show: $.[id,name]
properties: relatedId, [[relatedNote]]
```
~~~
这将创建:
```yaml
---
relatedId: 123
relatedNote: "[[Note Name]]"
---
```
!!! tip "用例"
非常适合从 API 响应中自动填充元数据。
---
## 💡 常见模式
### 身份验证流程
1. 获取身份验证令牌(隐藏):
~~~markdown
```req
url: https://api.example.com/auth/login
method: post
body: {"username": "{{this.username}}", "password": "{{this.password}}"}
req-uuid: auth
hidden
```
~~~
2. 在后续请求中使用令牌:
~~~markdown
```req
url: https://api.example.com/user/data
headers: {"Authorization": "Bearer {{ls.auth>$.token}}"}
```
~~~
### 错误处理
如果请求失败,将显示错误消息:
```
Error: Failed to fetch
```
常见错误:
- **网络错误**: 检查你的互联网连接
- **401 未授权**: 检查你的 API 密钥/令牌
- **404 未找到**: 验证 URL 是否正确
- **无效 JSONPath**: 检查你的 `show:` 表达式
### 性能提示
1. **缓存响应** - 对于不经常更改的数据使用 `req-uuid`
2. **使用 `disabled`** - 在开发期间临时禁用昂贵的请求
3. **使用 `hidden`** - 隐藏后台数据获取的输出
---
## 📚 扩展阅读
- [JSONPath 语法示例](https://github.com/JSONPath-Plus/JSONPath?tab=readme-ov-file#syntax-through-examples)
- [JSONPath 在线测试工具](https://jsonpath-plus.github.io/JSONPath/demo/)
- [用例](usecase/index.md) - 真实世界示例
---
## ❓ 故障排除
**请求不工作?**
- 检查控制台 (Ctrl+Shift+I) 查看详细错误消息
- 验证你的 URL 是否正确并使用 https://
- 检查是否存在所有必需的标志
- 确保你的 JSONPath 表达式有效
**缓存数据未更新?**
- 删除 `req-uuid` 以始终获取新数据
- 或添加 `auto-update` 标志以强制刷新
- 或在设置中手动清除缓存
**变量未解析?**
- 检查前置元数据语法是否正确
- 验证全局变量是否在设置中定义
- 确保你使用正确的变量语法
---
!!! tip "需要帮助?"
如果你遇到问题或有疑问,请在 [GitHub](https://github.com/Rooyca/obsidian-api-request/issues) 上提出问题。

View file

@ -1,20 +1,40 @@
# 🔎 概述
APIRequest (APIR) 是笔记应用 [Obsidian](https://obsidian.md/) 的一个插件,它允许您向 api 或其他来源发出请求并在笔记中显示响应。
APIRequest (APIR) 是一个用于笔记应用 [Obsidian](https://obsidian.md/) 的插件,允许您向 API 发出请求并直接在笔记中显示响应。
## 🔥 功能
## 🔥 功能特性
- 使用各种方法执行 HTTP 请求,例如 `GET`、`POST`、`PUT` 和 `DELETE`
- 在代码块内利用前言中的变量。
- 将响应保存在 `localStorage` 中,以方便访问和重用。
- 根据需要禁用代码块以优化性能。
- 以指定的间隔多次重复请求,促进自动化任务或连续数据检索而无需人工干预。
- **多种 HTTP 方法**:使用 `GET`、`POST`、`PUT`、`DELETE`、`PATCH`、`HEAD` 和 `OPTIONS` 执行请求。
- **变量替换**:利用 `front-matter` 中的变量、全局变量,甚至重用其他代码块的响应。
- **响应缓存**:将响应保存在 `localStorage` 中,方便跨笔记访问和重用。
- **性能控制**:根据需要禁用代码块以优化性能。
- **精确数据提取**:使用 JSONPath 显示响应中的特定值,提供对数据呈现的细粒度控制。
- **安全优先**:内置输入验证和清理,防止 XSS、注入攻击和目录遍历。
- **自动更新**:在需要时自动刷新缓存的响应。
- **格式化输出**:为响应数据自定义 HTML/文本格式。
## 🔒 安全功能
APIRequest 实现了全面的安全措施:
- ✅ **URL 验证**:仅允许 HTTPS 和 HTTP 协议
- ✅ **输入清理**:所有用户输入都经过验证和清理
- ✅ **XSS 防护**:清理 HTML 输出以防止脚本注入
- ✅ **目录遍历保护**:验证文件路径以防止未授权访问
- ✅ **安全 JSONPath**:在执行前验证 JSONPath 表达式
- ✅ **UUID 清理**:清理请求标识符以防止注入攻击
!!! warning "安全最佳实践"
- 进行 API 请求时始终使用 HTTPS URL
- 将 API 密钥存储在全局变量中(设置 → APIRequest → 全局变量),而不是笔记中
- 定期审查和清除缓存的响应(设置 → APIRequest → 已保存的 API 请求)
- 仅连接到受信任的 API 端点
## ⚡ 如何使用
### 👨🏻‍💻 代码块
要使用它,请创建一个代码块,并将语言设置为 `req`。在代码块内,您可以指定`url`、`method`、`body`、`headers` 等。有关更多信息,请参阅[可用标志](codeblocks.md#flags)。
要使用它,创建一个语言设置为 `req` 的代码块。在代码块内,您可以指定 `url`、`method`、`body`、`headers` 等。有关更多信息,请参阅[可用标志](codeblocks.md#flags)。
~~~markdown
```req
@ -23,7 +43,49 @@ method: post
body: {"id":1}
headers: {"Accept": "application/json"}
show: $.id
req-uuid: id-persona
req-uuid: IDpersona
disabled
```
~~~
## 📚 快速开始
1. **简单的 GET 请求**
~~~markdown
```req
url: https://api.github.com/users/octocat
show: $.name
```
~~~
2. **使用变量**
~~~markdown
```req
url: https://api.example.com/user/{{this.userId}}
headers: {"Authorization": "Bearer {{API_TOKEN}}"}
show: $.data.name
```
~~~
3. **缓存响应**
~~~markdown
```req
url: https://api.example.com/data
req-uuid: mydata
show: $.result
```
~~~
4. **重用缓存数据**
~~~markdown
```req
url: https://api.example.com/more-data
headers: {"X-Token": "{{ls.mydata>$.token}}"}
```
~~~
有关所有标志和功能的详细文档,请参阅[代码块](codeblocks.md)。

View file

@ -1,9 +1,34 @@
# 用例
# 使
此插件的用例集合。 **如果您有,请与我们分享。**
这是该插件的使用案例集合。**如果您有其他使用案例,请与我们分享。**
## 使用内联响应与 [Dataview](https://blacksmithgu.github.io/obsidian-dataview/) 结合
## 检查比特币(或任何加密货币)价格
首先,使用 `req-uuid` 发起请求并存储响应:
~~~markdown
```req
url: https://jsonplaceholder.typicode.com/comments/1
req-uuid: test
hidden
```
~~~
然后,启用 **DataviewJS** 和内联 JavaScript 查询后,您可以像这样访问保存的数据:
```markdown
邮箱是 `$=dv.el("span", JSON.parse(localStorage.getItem("req-test")).email)`
ID 是 `$=dv.el("span", JSON.parse(localStorage.getItem("req-test")).id, { cls: "mod-warning" })`
```
这将从保存的响应中检索邮箱和 ID始终需要 `req-` 前缀)。
在这里,我们还为第二个内联查询添加了自定义类。
渲染输出将如下所示:
> 邮箱是 [Eliseo@gardner.biz](mailto:Eliseo@gardner.biz)ID 是 <span style="color:red;">1</span>
## 查询比特币(或任何加密货币)价格
~~~makdown
```req
@ -14,7 +39,7 @@ show: $.data.rateUsd
> 64992.8972508856324769
## 获取天气
## 获取天气信息
~~~makdown
```req
@ -32,18 +57,19 @@ show: $.results[0:].title
```
~~~
!!! info "请注意使用 `{{this.title}}`。此功能允许您传递前置属性。"
!!! info "注意 `{{this.title}}` 的使用。这是一个允许您传递 front-matter 属性的功能。"
## 获得多个结果
## 渲染数据
~~~makdown
~~~markdown
```req
url: https://mapi.mobilelegends.com/hero/detail?id={{this.file.name}}
url: https://mapi.mobilelegends.com/hero/detail?id=1
show: $.data[cover_picture,name,type]
format: ![img]({}) <br> <strong>Name:</strong> {} <br> <strong>Type:</strong> {}
```
~~~
## 从 [todoist](https://todoist.com/) 获取 TODOS
## 从 [todoist](https://todoist.com/) 获取待办事项
~~~makdown
```req
@ -55,8 +81,8 @@ req-id: todos
```
~~~
!!! warning "这将把响应保存在 localStorage 中的键 `req-todos`"
!!! warning "这将把响应保存在 localStorage 中,键名为 `req-todos`"
## 您的用例
## 您的使
> **如果您想分享您的用例,请随时打开 PR 或 [Issue](https://github.com/Rooyca/obsidian-api-request/issues/new/choose)。**
> **如果您想分享您的使用案例,请随时提交 PR 或创建 [Issue](https://github.com/Rooyca/obsidian-api-request/issues/new/choose)。**

View file

@ -1,7 +1,7 @@
{
"id": "api-request",
"name": "APIRequest",
"version": "2.0.8",
"version": "2.0.9",
"minAppVersion": "0.15.0",
"description": "Integrate API data into your notes with request caching, variable support, and precise JSON extraction.",
"author": "rooyca",

2
package-lock.json generated
View file

@ -1,6 +1,6 @@
{
"name": "api-request",
"version": "2.0.8",
"version": "2.0.9",
"lockfileVersion": 3,
"requires": true,
"packages": {

View file

@ -1,6 +1,6 @@
{
"name": "api-request",
"version": "2.0.8",
"version": "2.0.9",
"description": "Integrate API data into your notes with request caching, variable support, and precise JSON extraction.",
"main": "main.js",
"scripts": {

View file

@ -1,17 +1,34 @@
// frontmatter read and parse
/**
* Utilities for reading and parsing frontmatter from markdown files
*/
import { parseYaml } from "obsidian";
/** Regular expression to extract frontmatter content from markdown */
export const FRONTMATTER_REGEX = /^\n*---[^\n]*\n+(?<fm>.+?)\n+---.*/s;
/** Type definition for frontmatter content */
export type Frontmatter = string | null | undefined;
/**
* Extracts frontmatter content from markdown text
*
* @param md - The markdown content to extract frontmatter from
* @returns The frontmatter content string, or undefined if not found
*/
export function readFrontmatter(md: string) {
const result = md.match(FRONTMATTER_REGEX);
return result?.groups?.fm;
}
/**
* Parses frontmatter YAML content into an object
*
* @param input - The frontmatter content to parse
* @returns Parsed frontmatter object
* @throws Error if frontmatter is not defined or cannot be parsed
*/
export function parseFrontmatter(input: Frontmatter) {
if (input === undefined || input === null) {
throw new Error("Frontmatter not defined.");

View file

@ -1,5 +1,11 @@
// Adds the copy button to the code block
// Take from: https://github.com/jdbrice/obsidian-code-block-copy/
/**
* Adds a copy button to the code block element
* Button copies the specified text to clipboard when clicked
*
* @param el - The HTML element to add the button to
* @param copyThis - The text to copy to clipboard
* @source https://github.com/jdbrice/obsidian-code-block-copy/
*/
export function addBtnCopy(el: HTMLElement, copyThis: string) {
const btnCopy = el.createEl("button", { cls: "copy-req", text: "copy" });
btnCopy.addEventListener('click', function () {

View file

@ -1,20 +1,24 @@
// Matches a specific pattern inside curly braces: {number1..number2}
/**
* Regular expression utilities for parsing request blocks
*/
/** Matches a numeric range pattern inside curly braces: {number1..number2} */
export const num_braces_regx = /{(\d+)\.\.(\d+)}/;
// Matches a pattern of digits separated by hyphens, e.g., 1-2-3-4
/** Matches a pattern of digits separated by hyphens, e.g., 1-2-3-4 */
export const num_hyphen_regx = /(\d+-)+\d+/;
// Matches any sequence of digits, globally.
/** Matches any sequence of digits, globally */
export const nums_rex = /\d+/g;
// Matches anything inside curly braces, non-greedy, globally.
/** Matches anything inside curly braces, non-greedy, globally */
export const in_braces_regx = /{.*?}/g;
// Matches any text inside double curly braces, preceded by "{{this." and followed by "}}"
/** Matches text inside double curly braces, preceded by "{{this." and followed by "}}" */
export const varname_regx = /{{this\.([^{}]*)}}/g;
// Matches "{{this." or "}}" globally
/** Matches "{{this." or "}}" globally - used for removing variable syntax */
export const no_varname_regx = /{{this\.|}}/g;
// Matches "{{KEY}}" globally
/** Matches "{{KEY}}" globally - for global variable replacement */
export const key_regx = /{{(?!this\.)[^{}]*}}/g;

196
src/functions/security.ts Normal file
View file

@ -0,0 +1,196 @@
/**
* Security utilities for input validation and sanitization
*/
/**
* Validates a URL to ensure it's safe to request
* @param url - The URL to validate
* @returns true if valid, false otherwise
*/
export function isValidUrl(url: string): boolean {
if (!url || typeof url !== 'string') {
return false;
}
try {
const parsed = new URL(url);
// Only allow http and https protocols
if (!['http:', 'https:'].includes(parsed.protocol)) {
return false;
}
// Prevent local network access (optional - can be configured)
// Uncomment to prevent requests to localhost/private IPs
// const hostname = parsed.hostname.toLowerCase();
// if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '0.0.0.0') {
// return false;
// }
return true;
} catch (e) {
return false;
}
}
/**
* Validates HTTP method
* @param method - The HTTP method to validate
* @returns true if valid, false otherwise
*/
export function isValidMethod(method: string): boolean {
const allowedMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
return allowedMethods.includes(method.toUpperCase());
}
/**
* Sanitizes a UUID to prevent injection attacks
* @param uuid - The UUID to sanitize
* @returns Sanitized UUID or null if invalid
*/
export function sanitizeUuid(uuid: string): string | null {
if (!uuid || typeof uuid !== 'string') {
return null;
}
// Only allow alphanumeric characters, hyphens, and underscores
const sanitized = uuid.replace(/[^a-zA-Z0-9\-_]/g, '');
// Limit length to prevent storage issues
if (sanitized.length > 100) {
return sanitized.substring(0, 100);
}
return sanitized || null;
}
/**
* Validates JSONPath expression to prevent malicious code execution
* @param jsonPath - The JSONPath expression to validate
* @returns true if valid, false otherwise
*/
export function isValidJsonPath(jsonPath: string): boolean {
if (!jsonPath || typeof jsonPath !== 'string') {
return false;
}
// Check for potential script injection
const dangerousPatterns = [
/<script/i,
/javascript:/i,
/on\w+=/i, // event handlers like onclick=
/eval\(/i,
/Function\(/i,
/setTimeout\(/i,
/setInterval\(/i,
];
for (const pattern of dangerousPatterns) {
if (pattern.test(jsonPath)) {
return false;
}
}
// Basic JSONPath should start with $ or be a simple path
if (!jsonPath.startsWith('$') && !jsonPath.startsWith('.')) {
return false;
}
return true;
}
/**
* Sanitizes HTML to prevent XSS attacks
* @param html - The HTML string to sanitize
* @returns Sanitized HTML string
*/
export function sanitizeHtml(html: string): string {
if (!html || typeof html !== 'string') {
return '';
}
// Remove script tags and event handlers
return html
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/on\w+\s*=\s*["'][^"']*["']/gi, '')
.replace(/on\w+\s*=\s*[^\s>]*/gi, '')
.replace(/javascript:/gi, '');
}
/**
* Validates format string to prevent XSS
* @param format - The format string to validate
* @returns true if valid, false otherwise
*/
export function isValidFormat(format: string): boolean {
if (!format || typeof format !== 'string') {
return true; // Empty format is valid
}
// Check for script tags and event handlers
const dangerousPatterns = [
/<script/i,
/javascript:/i,
/on\w+=/i,
/<iframe/i,
/<embed/i,
/<object/i,
];
for (const pattern of dangerousPatterns) {
if (pattern.test(format)) {
return false;
}
}
return true;
}
/**
* Validates file path to prevent directory traversal attacks
* @param filePath - The file path to validate
* @returns true if valid, false otherwise
*/
export function isValidFilePath(filePath: string): boolean {
if (!filePath || typeof filePath !== 'string') {
return false;
}
// Prevent directory traversal
if (filePath.includes('..') || filePath.includes('//')) {
return false;
}
// Prevent absolute paths (should be relative to vault)
if (filePath.startsWith('/') || filePath.match(/^[a-zA-Z]:\\/)) {
return false;
}
return true;
}
/**
* Safely parse JSON with error handling
* @param jsonString - The JSON string to parse
* @returns Parsed object or null if invalid
*/
export function safeJsonParse(jsonString: string): any {
try {
return JSON.parse(jsonString);
} catch (e) {
return null;
}
}
/**
* Validates localStorage key to prevent attacks
* @param key - The localStorage key to validate
* @returns true if valid, false otherwise
*/
export function isValidStorageKey(key: string): boolean {
if (!key || typeof key !== 'string') {
return false;
}
// Only allow safe characters
return /^[a-zA-Z0-9\-_]+$/.test(key);
}

View file

@ -5,6 +5,8 @@ import {
requestUrl,
debounce,
Menu,
TFile,
TAbstractFile,
} from "obsidian";
import {
readFrontmatter,
@ -12,12 +14,61 @@ import {
} from "src/functions/frontmatterUtils";
import { addBtnCopy } from "src/functions/general";
import { varname_regx, no_varname_regx, key_regx } from "src/functions/regx";
import {
isValidUrl,
isValidMethod,
sanitizeUuid,
isValidJsonPath,
sanitizeHtml,
isValidFormat,
isValidFilePath,
safeJsonParse,
isValidStorageKey
} from "src/functions/security";
import APRSettings from "src/settings/settingsTab";
import { JSONPath } from "jsonpath-plus";
import { LoadAPIRSettings, DEFAULT_SETTINGS } from "src/settings/settingsData";
// Get global variables (defined in settings)
export function checkGlobalValue(value: string, settings: LoadAPIRSettings) {
/**
* Interface for tracking request code blocks in the editor
*/
interface ReqCodeBlock {
/** Unique identifier for the request (optional) */
uuid: string | null;
/** Index of the block in the document */
index: number;
/** Line number where the code block starts */
lineStart: number;
/** Whether the request is disabled */
disabled: boolean;
/** Whether the request should auto-update */
autoUpdate: boolean;
/** Whether the request is active (for UI styling) */
isActive: boolean;
/** Display name for the request in the UI */
displayName: string;
}
/**
* Interface for key-value pairs in plugin settings
*/
interface KeyValuePair {
/** The key identifier */
key: string;
/** The value associated with the key */
value: string;
}
/**
* Retrieves global variables defined in plugin settings and replaces them in the input string
* @param value - The string containing variable placeholders in the format {{KEY}}
* @param settings - The plugin settings containing key-value pairs
* @returns The string with variables replaced by their values
* @example
* // If settings has {key: "API_KEY", value: "12345"}
* checkGlobalValue("url={{API_KEY}}", settings) // returns "url=12345"
*/
export function checkGlobalValue(value: string, settings: LoadAPIRSettings): string {
const match = value.match(key_regx);
if (match) {
@ -25,7 +76,7 @@ export function checkGlobalValue(value: string, settings: LoadAPIRSettings) {
const key = match[i].replace(/{{|}}/g, "");
value = value.replace(
match[i],
settings.KeyValueCodeblocks.find((obj) => obj.key === key)
(settings.KeyValueCodeblocks as KeyValuePair[]).find((obj) => obj.key === key)
?.value || "",
);
}
@ -33,9 +84,18 @@ export function checkGlobalValue(value: string, settings: LoadAPIRSettings) {
return value;
}
// Get data from localStorage using this syntax: {{ls.UUID>JSONPath}}
// where `ls` stands for `localStorage`
export function checkLocalStorage(value: string) {
/**
* Retrieves data from localStorage using the syntax {{ls.UUID>JSONPath}}
* where 'ls' stands for 'localStorage'
*
* @param value - The string containing localStorage references
* @returns The string with localStorage references replaced by their values
* @security Validates UUID format and JSONPath expressions to prevent injection attacks
* @example
* // If localStorage has "req-mydata" with {user: {name: "John"}}
* checkLocalStorage("{{ls.mydata>$.user.name}}") // returns "John"
*/
export function checkLocalStorage(value: string): string {
const match = value.match(key_regx);
if (match) {
@ -43,28 +103,57 @@ export function checkLocalStorage(value: string) {
const key = match[i].replace(/{{|}}/g, "");
let uuid = key.split(">")[0];
const jsonPath = key.split(">")[1];
uuid = uuid.split(".")[1]
uuid = uuid.split(".")[1];
// Validate uuid and jsonPath for security
const sanitizedUuid = sanitizeUuid(uuid);
if (!sanitizedUuid) {
console.warn("Invalid UUID format:", uuid);
continue;
}
if (jsonPath && !isValidJsonPath(jsonPath)) {
console.warn("Invalid JSONPath expression:", jsonPath);
continue;
}
// Try with req- prefix first (for req-uuid data)
let data = localStorage.getItem(`req-${uuid}`);
let data = localStorage.getItem(`req-${sanitizedUuid}`);
// If not found, try without prefix (for manual variables)
if (!data) {
data = localStorage.getItem(uuid);
data = localStorage.getItem(sanitizedUuid);
}
if (data) {
const parsedData = JSON.parse(data);
const output = JSONPath({ path: jsonPath, json: parsedData });
value = value.replace(match[i], output);
const parsedData = safeJsonParse(data);
if (parsedData && jsonPath) {
try {
const output = JSONPath({ path: jsonPath, json: parsedData });
value = value.replace(match[i], output);
} catch (e) {
console.error("JSONPath evaluation error:", e);
}
}
}
}
}
return value;
}
// parse headers and body to valid JSON
export function parseToValidJson(input, type) {
/**
* Parses and validates input string to JSON format
* Attempts to convert various quote styles to valid JSON
*
* @param input - The input string to parse
* @param type - The type of data being parsed (for error messages)
* @returns Parsed JSON object or null if input is empty
* @throws Error if the input cannot be parsed to valid JSON
* @example
* parseToValidJson("{key: value}", "headers") // returns {"key": "value"}
* parseToValidJson("'key': 'value'", "body") // returns {"key": "value"}
*/
export function parseToValidJson(input: string, type: string): Record<string, any> | null {
const trimmedInput = input.trim();
if (!trimmedInput) {
@ -89,7 +178,16 @@ export function parseToValidJson(input, type) {
}
}
// format the output to be displayed
/**
* Formats output for display in the code block
* Handles arrays, objects, and primitive values
*
* @param output - The output to format (can be any type)
* @returns Formatted string representation of the output
* @example
* formatOutput([1, 2, 3]) // returns "1, 2, 3"
* formatOutput({key: "value"}) // returns '{\n "key": "value"\n}'
*/
export function formatOutput(output: any): string {
// If output is Array
if (Array.isArray(output)) {
@ -113,54 +211,98 @@ export function formatOutput(output: any): string {
return String(output ?? "");
}
// Check if the value has variables and replace them
export function checkVariables(req_prop: string, settings: LoadAPIRSettings) {
// search value in localStorage
req_prop = checkLocalStorage(req_prop);
// search value globally
req_prop = checkGlobalValue(req_prop, settings);
const match = req_prop.match(varname_regx);
/**
* Checks if the value contains variables and replaces them with actual values
* Supports:
* - localStorage references: {{ls.UUID>JSONPath}}
* - Global variables: {{KEY}}
* - Frontmatter variables: {{this.propertyName}}
* - File name: {{this.file.name}}
*
* @param req_prop - The string containing variable placeholders
* @param settings - The plugin settings
* @returns The string with variables replaced, or undefined if an error occurs
* @security All variable values are validated before substitution
* @example
* checkVariables("{{this.file.name}}", settings) // returns current file name
* checkVariables("{{API_KEY}}", settings) // returns value from global settings
*/
export function checkVariables(req_prop: string, settings: LoadAPIRSettings): string | undefined {
try {
// search value in localStorage
req_prop = checkLocalStorage(req_prop);
// search value globally
req_prop = checkGlobalValue(req_prop, settings);
const match = req_prop.match(varname_regx);
if (match) {
for (let i = 0; i < match.length; i++) {
const var_name = match[i].replace(no_varname_regx, "");
if (match) {
for (let i = 0; i < match.length; i++) {
const var_name = match[i].replace(no_varname_regx, "");
// if {{this.file.name}} return filename
if (var_name == "file.name") {
req_prop = req_prop.replace(
match[i],
this.app.workspace.getActiveFile().basename,
);
continue;
}
// if {{this.file.name}} return filename
if (var_name == "file.name") {
const activeFile = this.app.workspace.getActiveFile();
if (!activeFile) {
console.warn("No active file found");
continue;
}
req_prop = req_prop.replace(
match[i],
activeFile.basename,
);
continue;
}
const activeView =
this.app.workspace.getActiveViewOfType(MarkdownView);
const markdownContent = activeView.editor.getValue();
const activeView =
this.app.workspace.getActiveViewOfType(MarkdownView);
if (!activeView) {
console.warn("No active markdown view found");
continue;
}
const markdownContent = activeView.editor.getValue();
try {
const frontmatterData = parseFrontmatter(
readFrontmatter(markdownContent),
);
req_prop = req_prop.replace(
match[i],
frontmatterData[var_name] || "",
);
} catch (e) {
console.error(e.message);
new Notice("Error: " + e.message);
return;
try {
const frontmatterData = parseFrontmatter(
readFrontmatter(markdownContent),
);
req_prop = req_prop.replace(
match[i],
frontmatterData[var_name] || "",
);
} catch (e: any) {
console.error("Frontmatter parsing error:", e.message);
new Notice("Error reading frontmatter: " + e.message);
return undefined;
}
}
}
return req_prop;
} catch (e: any) {
console.error("Variable substitution error:", e);
new Notice("Error processing variables: " + e.message);
return undefined;
}
return req_prop;
}
/**
* Main plugin class for API Request functionality
* Allows users to make HTTP requests directly from Obsidian code blocks
* and display responses with caching, variable substitution, and data extraction
*/
export default class MainAPIR extends Plugin {
/** Plugin settings */
settings: LoadAPIRSettings;
/** Status bar element for displaying request count */
private statusBarItem: HTMLElement;
/** List of request code blocks in the current file */
private reqBlocks: ReqCodeBlock[] = [];
/**
* Called when the plugin is loaded
* Registers code block processor, event handlers, and settings tab
*/
async onload() {
console.log("Loading: api-request");
await this.loadSettings();
@ -228,7 +370,7 @@ export default class MainAPIR extends Plugin {
// get the method and check if is a valid method
} else if (lowercaseLine.startsWith("method:")) {
method = line.replace(/method:/i, "").toUpperCase().trim();
if (!allowedMethods.includes(method)) {
if (!isValidMethod(method)) {
el.createEl("strong", {
text: `Error: Method ${method} not supported`,
});
@ -248,6 +390,14 @@ export default class MainAPIR extends Plugin {
});
return;
}
// Validate URL for security
if (!isValidUrl(URL)) {
el.createEl("strong", {
text: "Error: Invalid or unsafe URL",
});
return;
}
// extract data using jsonpath-plus (https://www.npmjs.com/package/jsonpath-plus)
} else if (lowercaseLine.startsWith("show:")) {
@ -262,6 +412,17 @@ export default class MainAPIR extends Plugin {
});
return;
}
// Validate JSONPath for security
const paths = show.split(" + ");
for (const path of paths) {
if (!isValidJsonPath(path.trim())) {
el.createEl("strong", {
text: "Error: Invalid JSONPath expression",
});
return;
}
}
// get headers. They can use double, single quotes or none
} else if (lowercaseLine.startsWith("headers:")) {
@ -276,8 +437,8 @@ export default class MainAPIR extends Plugin {
tempHeaders,
"headers",
);
} catch (e) {
el.createEl("strong", { text: e.message });
} catch (e: any) {
el.createEl("strong", { text: e.message || "Error parsing headers" });
return;
}
@ -291,8 +452,9 @@ export default class MainAPIR extends Plugin {
try {
body = parseToValidJson(tempBody, "body");
} catch {
} catch (e: any) {
// use raw body if it's not a valid JSON
console.log("Using raw body (not valid JSON):", e.message);
body = tempBody;
}
@ -305,6 +467,14 @@ export default class MainAPIR extends Plugin {
});
return;
}
// Validate file path for security
if (!isValidFilePath(saveTo)) {
el.createEl("strong", {
text: "Error: Invalid file path. Path traversal and absolute paths are not allowed.",
});
return;
}
} else if (lowercaseLine.startsWith("req-uuid:")) {
uuid = line.replace(/req-uuid:/i, "").trim();
if (!uuid) {
@ -313,18 +483,35 @@ export default class MainAPIR extends Plugin {
});
return;
}
uuid =
uuid =
checkVariables(
uuid,
this.settings,
) ?? "";
uuid = `req-${uuid}`
// Sanitize UUID for security
const sanitized = sanitizeUuid(uuid);
if (!sanitized) {
el.createEl("strong", {
text: "Error: Invalid UUID format",
});
return;
}
uuid = `req-${sanitized}`;
} else if (lowercaseLine.startsWith("auto-update")) {
autoUpdate = true;
} else if (lowercaseLine.startsWith("hidden")) {
hidden = true;
} else if (lowercaseLine.startsWith("hidden")) {
hidden = true;
} else if (lowercaseLine.startsWith("format:")) {
format = line.replace(/format:/i, "").trim();
// Validate format for XSS prevention
if (!isValidFormat(format)) {
el.createEl("strong", {
text: "Error: Invalid format string. Script tags and event handlers are not allowed.",
});
return;
}
} else if (lowercaseLine.startsWith("properties:")) {
properties = line
.replace(/properties:/i, "")
@ -333,23 +520,31 @@ export default class MainAPIR extends Plugin {
}
}
let responseData;
let responseDataText;
let responseData: any;
let responseDataText: string | undefined;
// Check if the response is cached in localStorage
if (uuid && !autoUpdate) {
const cachedResponse = localStorage.getItem(uuid);
if (cachedResponse) {
responseData = JSON.parse(cachedResponse);
const temp_uuid = uuid.split("req-")[1]
new Notice(`Using cached data with UUID: ${temp_uuid}`);
try {
const cachedResponse = localStorage.getItem(uuid);
if (cachedResponse) {
responseData = safeJsonParse(cachedResponse);
if (responseData) {
const temp_uuid = uuid.split("req-")[1];
new Notice(`Using cached data with UUID: ${temp_uuid}`);
} else {
console.warn("Failed to parse cached data");
}
}
} catch (e) {
console.error("Error reading from localStorage:", e);
}
}
// If no cached data or auto-update is requested, make a new request
if (!responseData || autoUpdate) {
try {
body = method == "GET" ? undefined : JSON.stringify(body);
body = method == "GET" ? undefined : JSON.stringify(body);
const response = await requestUrl({
url: URL,
method,
@ -361,12 +556,17 @@ export default class MainAPIR extends Plugin {
// Cache the response in localStorage if req-uuid is provided
if (uuid) {
localStorage.setItem(
uuid,
JSON.stringify(responseData),
);
try {
localStorage.setItem(
uuid,
JSON.stringify(responseData),
);
} catch (e) {
console.error("Error saving to localStorage:", e);
new Notice("Warning: Failed to cache response");
}
}
} catch (e) {
} catch (e: any) {
console.error(e.message);
new Notice("Error: " + e.message);
responseData = `Error: ${e.message}`;
@ -382,8 +582,22 @@ export default class MainAPIR extends Plugin {
// check if the user defined more than one path
// if so, iterate over each path and get the data
output = show.length > 1
? show.map((path) => JSONPath({ path: path.trim(), json: responseData }))
: JSONPath({ path: show[0], json: responseData });
? show.map((path) => {
try {
return JSONPath({ path: path.trim(), json: responseData });
} catch (e: any) {
console.error("JSONPath error for path:", path, e);
return `Error: ${e.message}`;
}
})
: (() => {
try {
return JSONPath({ path: show[0], json: responseData });
} catch (e: any) {
console.error("JSONPath error:", e);
return `Error: ${e.message}`;
}
})();
if (properties.length > 0 && properties[0] !== '') {
// Format the output and split it into an array
@ -446,26 +660,41 @@ export default class MainAPIR extends Plugin {
// try to create the file. It'll fail if already exists
await this.app.vault.create(
saveTo,
responseDataText,
responseDataText || "",
);
new Notice("Saved to: " + saveTo);
} catch (e) {
} catch (e: any) {
// try to modify the file
const file =
this.app.vault.getAbstractFileByPath(saveTo);
await this.app.vault.modify(file, responseDataText);
new Notice("File modified");
try {
const file = this.app.vault.getAbstractFileByPath(saveTo);
if (file instanceof TFile) {
await this.app.vault.modify(file, responseDataText || "");
new Notice("File modified");
} else {
new Notice("Error: Could not save to file");
console.error("File save error:", e);
}
} catch (modifyError: any) {
new Notice("Error: Failed to save file");
console.error("File modification error:", modifyError);
}
}
}
// if a *format* is defined in the codeblock
// render the response, else just *return* the response as String
if (hidden) {
return;
return;
} else if (format) {
const parts = formattedOutput.split(",");
el.innerHTML = format.replace(/{}/g, () => parts.shift() || "");
} else {
// Sanitize the format output to prevent XSS
const sanitizedFormat = sanitizeHtml(format);
el.innerHTML = sanitizedFormat.replace(/{}/g, () => {
const part = parts.shift() || "";
// Escape the part to prevent XSS
return sanitizeHtml(part);
});
} else {
el.createEl("pre", { text: formattedOutput });
}
@ -481,7 +710,12 @@ export default class MainAPIR extends Plugin {
this.addSettingTab(new APRSettings(this.app, this));
}
// Parse all req code blocks from the active file
/**
* Parses all req code blocks from the active file
* Extracts metadata like UUID, disabled status, and auto-update flag
*
* @returns Array of parsed request code blocks
*/
private parseReqBlocks(): ReqCodeBlock[] {
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (!view) return [];
@ -549,8 +783,11 @@ export default class MainAPIR extends Plugin {
return blocks;
}
// Update the status bar display
private updateStatusBar() {
/**
* Updates the status bar display with request count
* Shows/hides based on settings and number of requests
*/
updateStatusBar() {
// Check if status bar is enabled
if (!this.settings.enableStatusBar) {
this.statusBarItem.style.display = 'none';
@ -585,7 +822,12 @@ export default class MainAPIR extends Plugin {
`;
}
// Show menu with all requests
/**
* Shows a menu with all available request blocks
* Allows navigation to specific blocks
*
* @param event - The mouse event that triggered the menu
*/
private showRequestMenu(event: MouseEvent) {
const menu = new Menu();
@ -600,15 +842,19 @@ export default class MainAPIR extends Plugin {
item.setTitle(block.displayName);
item.setIcon('rocket');
// Add color styling using settings colors
const color = block.isActive
? this.settings.statusBarActiveColor
: this.settings.statusBarInactiveColor;
item.dom.style.color = color;
// Store color in a CSS variable that can be used by styles
const isActive = block.isActive;
item.onClick(() => {
this.navigateToBlock(block);
});
// Add custom styling via callback
const color = isActive
? this.settings.statusBarActiveColor
: this.settings.statusBarInactiveColor;
// Use setIcon with color hint through title
item.setTitle(`${block.displayName}${isActive ? ' 🟢' : ' ⚪'}`);
});
});
}
@ -616,7 +862,12 @@ export default class MainAPIR extends Plugin {
menu.showAtMouseEvent(event);
}
// Navigate to the code block in the editor
/**
* Navigates to a specific code block in the editor
* Scrolls to the block and selects it for visual feedback
*
* @param block - The request block to navigate to
*/
private navigateToBlock(block: ReqCodeBlock) {
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (!view) {
@ -656,10 +907,16 @@ export default class MainAPIR extends Plugin {
);
}
/**
* Called when the plugin is unloaded
*/
onunload() {
console.log("Unloading: api-request");
}
/**
* Loads plugin settings from storage
*/
async loadSettings() {
this.settings = Object.assign(
{},
@ -668,6 +925,9 @@ export default class MainAPIR extends Plugin {
);
}
/**
* Saves plugin settings to storage
*/
async saveSettings() {
await this.saveData(this.settings);
}

View file

@ -1,13 +1,37 @@
/**
* Interface for key-value pairs in plugin settings
*/
interface KeyValuePair {
/** The key identifier */
key: string;
/** The value associated with the key */
value: string;
}
/**
* Plugin settings interface
* Contains all user-configurable options for the API Request plugin
*/
export interface LoadAPIRSettings {
/** Text to display when a request is disabled */
DisabledReq: string;
/** Temporary key input field value */
Key: string;
/** Temporary value input field value */
Value: string;
KeyValueCodeblocks: object[];
/** Array of global key-value pairs for variable substitution */
KeyValueCodeblocks: KeyValuePair[];
/** Whether to show the interactive status bar */
enableStatusBar: boolean;
/** Color for active requests in status bar (hex color) */
statusBarActiveColor: string;
/** Color for inactive/disabled requests in status bar (hex color) */
statusBarInactiveColor: string;
}
/**
* Default plugin settings
*/
export const DEFAULT_SETTINGS: LoadAPIRSettings = {
DisabledReq: '>> Disabled <<',
Key: '',