Browse Source

Add markdown editor

Add markdown editor so anyone can test it. Fix explode on tags.
pull/9/head
Danang Probo Sayekti 12 years ago
parent
commit
e829ae974f
22 changed files with 4398 additions and 5 deletions
  1. +0
    -0
      admin/config.ini
  2. +32
    -0
      admin/editor/LICENSE.txt
  3. +0
    -0
      admin/editor/README.txt
  4. +141
    -0
      admin/editor/css/editor.css
  5. BIN
      admin/editor/img/wmd-buttons.png
  6. +1412
    -0
      admin/editor/js/Markdown.Converter.js
  7. +2212
    -0
      admin/editor/js/Markdown.Editor.js
  8. +108
    -0
      admin/editor/js/Markdown.Sanitizer.js
  9. +43
    -0
      admin/editor/js/local/Markdown.local.fr.js
  10. +2
    -0
      admin/editor/js/node-pagedown.js
  11. +12
    -0
      admin/editor/package.json
  12. +18
    -0
      admin/includes/auth.php
  13. +80
    -0
      admin/includes/create_post.php
  14. +72
    -0
      admin/includes/edit_post.php
  15. +32
    -0
      admin/includes/login.php
  16. +9
    -0
      admin/includes/logout.php
  17. +134
    -0
      admin/includes/post_list.php
  18. +16
    -0
      admin/includes/session.php
  19. +20
    -0
      admin/index.php
  20. +42
    -0
      admin/resources/style.css
  21. +1
    -0
      admin/users/.htaccess
  22. +12
    -5
      system/includes/functions.php

system/config.ini → admin/config.ini View File


+ 32
- 0
admin/editor/LICENSE.txt View File

@ -0,0 +1,32 @@
A javascript port of Markdown, as used on Stack Overflow
and the rest of Stack Exchange network.
Largely based on showdown.js by John Fraser (Attacklab).
Original Markdown Copyright (c) 2004-2005 John Gruber
<http://daringfireball.net/projects/markdown/>
Original Showdown code copyright (c) 2007 John Fraser
Modifications and bugfixes (c) 2009 Dana Robinson
Modifications and bugfixes (c) 2009-2013 Stack Exchange Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

+ 0
- 0
admin/editor/README.txt View File


+ 141
- 0
admin/editor/css/editor.css View File

@ -0,0 +1,141 @@
body {
font-family: sans-serif;
}
blockquote {
border-left: 2px dotted #888;
padding-left: 5px;
background: #d0f0ff;
}
.wmd-panel{
width: 46%;
float:left;
padding:2%;
}
.wmd-button-bar {
background-color: #FEFEFE;
padding: 5px 0;
margin-bottom:10px;
width: 100%;
}
.wmd-input {
height: 300px;
width: 96%;
background-color: #FFFFFF;
border:none;
padding: 2%;
}
.wmd-preview {
background-color: #c0e0ff;
width: 46%;
float:left;
padding:2%;
}
.wmd-button-row {
position: relative;
margin:0px;
padding: 0px;
height: 20px;
}
.wmd-spacer{
width: 1px;
height: 20px;
margin-left: 14px;
position: absolute;
background-color: Silver;
display: inline-block;
list-style: none;
}
.wmd-button {
width: 20px;
height: 20px;
padding-left: 2px;
padding-right: 3px;
position: absolute;
display: inline-block;
list-style: none;
cursor: pointer;
}
.wmd-button > span {
background-image: url(../img/wmd-buttons.png);
background-repeat: no-repeat;
background-position: 0px 0px;
width: 20px;
height: 20px;
display: inline-block;
}
.wmd-spacer1{
left: 50px;
}
.wmd-spacer2{
left: 175px;
}
.wmd-spacer3{
left: 300px;
}
.wmd-prompt-background{
background-color: Black;
}
.wmd-prompt-dialog{
border: 1px solid #999999;
background-color: #F5F5F5;
}
.wmd-prompt-dialog > div {
font-size: 0.8em;
font-family: arial, helvetica, sans-serif;
}
.wmd-prompt-dialog > form > input[type="text"] {
border: 1px solid #999999;
color: black;
}
.wmd-prompt-dialog > form > input[type="button"]{
border: 1px solid #888888;
font-family: trebuchet MS, helvetica, sans-serif;
font-size: 0.8em;
font-weight: bold;
}
pre {
margin: 1em 0;
overflow: auto;
background: #F1F1FF;
}
pre code {
color: #333333;
display: block;
font-family: Consolas,Menlo,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace,serif;
font-size: 14px;
padding: 5px 10px;
}
@media all and (max-width: 980px) {
.wmd-panel, .wmd-preview {
width: 96%;
float:left;
padding:2%;
}
pre {
white-space: pre-wrap;
word-wrap:break-word;
}
}

BIN
admin/editor/img/wmd-buttons.png View File

Before After
Width: 260  |  Height: 60  |  Size: 7.3 KiB

+ 1412
- 0
admin/editor/js/Markdown.Converter.js
File diff suppressed because it is too large
View File


+ 2212
- 0
admin/editor/js/Markdown.Editor.js
File diff suppressed because it is too large
View File


+ 108
- 0
admin/editor/js/Markdown.Sanitizer.js View File

@ -0,0 +1,108 @@
(function () {
var output, Converter;
if (typeof exports === "object" && typeof require === "function") { // we're in a CommonJS (e.g. Node.js) module
output = exports;
Converter = require("./Markdown.Converter").Converter;
} else {
output = window.Markdown;
Converter = output.Converter;
}
output.getSanitizingConverter = function () {
var converter = new Converter();
converter.hooks.chain("postConversion", sanitizeHtml);
converter.hooks.chain("postConversion", balanceTags);
return converter;
}
function sanitizeHtml(html) {
return html.replace(/<[^>]*>?/gi, sanitizeTag);
}
// (tags that can be opened/closed) | (tags that stand alone)
var basic_tag_whitelist = /^(<\/?(b|blockquote|code|del|dd|dl|dt|em|h1|h2|h3|i|kbd|li|ol|p|pre|s|sup|sub|strong|strike|ul)>|<(br|hr)\s?\/?>)$/i;
// <a href="url..." optional title>|</a>
var a_white = /^(<a\shref="((https?|ftp):\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+"(\stitle="[^"<>]+")?\s?>|<\/a>)$/i;
// <img src="url..." optional width optional height optional alt optional title
var img_white = /^(<img\ssrc="(https?:\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+"(\swidth="\d{1,3}")?(\sheight="\d{1,3}")?(\salt="[^"<>]*")?(\stitle="[^"<>]*")?\s?\/?>)$/i;
function sanitizeTag(tag) {
if (tag.match(basic_tag_whitelist) || tag.match(a_white) || tag.match(img_white))
return tag;
else
return "";
}
/// <summary>
/// attempt to balance HTML tags in the html string
/// by removing any unmatched opening or closing tags
/// IMPORTANT: we *assume* HTML has *already* been
/// sanitized and is safe/sane before balancing!
///
/// adapted from CODESNIPPET: A8591DBA-D1D3-11DE-947C-BA5556D89593
/// </summary>
function balanceTags(html) {
if (html == "")
return "";
var re = /<\/?\w+[^>]*(\s|$|>)/g;
// convert everything to lower case; this makes
// our case insensitive comparisons easier
var tags = html.toLowerCase().match(re);
// no HTML tags present? nothing to do; exit now
var tagcount = (tags || []).length;
if (tagcount == 0)
return html;
var tagname, tag;
var ignoredtags = "<p><img><br><li><hr>";
var match;
var tagpaired = [];
var tagremove = [];
var needsRemoval = false;
// loop through matched tags in forward order
for (var ctag = 0; ctag < tagcount; ctag++) {
tagname = tags[ctag].replace(/<\/?(\w+).*/, "$1");
// skip any already paired tags
// and skip tags in our ignore list; assume they're self-closed
if (tagpaired[ctag] || ignoredtags.search("<" + tagname + ">") > -1)
continue;
tag = tags[ctag];
match = -1;
if (!/^<\//.test(tag)) {
// this is an opening tag
// search forwards (next tags), look for closing tags
for (var ntag = ctag + 1; ntag < tagcount; ntag++) {
if (!tagpaired[ntag] && tags[ntag] == "</" + tagname + ">") {
match = ntag;
break;
}
}
}
if (match == -1)
needsRemoval = tagremove[ctag] = true; // mark for removal
else
tagpaired[match] = true; // mark paired
}
if (!needsRemoval)
return html;
// delete all orphaned tags from the string
var ctag = 0;
html = html.replace(re, function (match) {
var res = tagremove[ctag] ? "" : match;
ctag++;
return res;
});
return html;
}
})();

+ 43
- 0
admin/editor/js/local/Markdown.local.fr.js View File

@ -0,0 +1,43 @@
// Usage:
//
// var myConverter = new Markdown.Editor(myConverter, null, { strings: Markdown.local.fr });
(function () {
Markdown.local = Markdown.local || {};
Markdown.local.fr = {
bold: "Gras <strong> Ctrl+B",
boldexample: "texte en gras",
italic: "Italique <em> Ctrl+I",
italicexample: "texte en italique",
link: "Hyperlien <a> Ctrl+L",
linkdescription: "description de l'hyperlien",
linkdialog: "<p><b>Insérer un hyperlien</b></p><p>http://example.com/ \"titre optionnel\"</p>",
quote: "Citation <blockquote> Ctrl+Q",
quoteexample: "Citation",
code: "Extrait de code <pre><code> Ctrl+K",
codeexample: "votre extrait de code",
image: "Image <img> Ctrl+G",
imagedescription: "description de l'image",
imagedialog: "<p><b>Insérer une image</b></p><p>http://example.com/images/diagram.jpg \"titre optionnel\"<br><br><a href='http://www.google.com/search?q=free+image+hosting' target='_blank'>Vous chercher un hébergement d'image grauit ?</a></p>",
olist: "Liste numérotée <ol> Ctrl+O",
ulist: "Liste à point <ul> Ctrl+U",
litem: "Elément de liste",
heading: "Titre <h1>/<h2> Ctrl+H",
headingexample: "Titre",
hr: "Trait horizontal <hr> Ctrl+R",
undo: "Annuler - Ctrl+Z",
redo: "Refaire - Ctrl+Y",
redomac: "Refaire - Ctrl+Shift+Z",
help: "Aide sur Markdown"
};
})();

+ 2
- 0
admin/editor/js/node-pagedown.js View File

@ -0,0 +1,2 @@
exports.Converter = require("./Markdown.Converter").Converter;
exports.getSanitizingConverter = require("./Markdown.Sanitizer").getSanitizingConverter;

+ 12
- 0
admin/editor/package.json View File

@ -0,0 +1,12 @@
{
"name": "pagedown",
"version": "1.1.0",
"description": "markdown converter, based on showdown",
"repository": { "type": "hg", "url": "https://code.google.com/p/pagedown/" },
"keywords": ["markdown"],
"license": "MIT",
"files": ["Markdown.Converter.js", "Markdown.Sanitizer.js", "node-pagedown.js"],
"main": "node-pagedown.js",
"bugs": "http://code.google.com/p/pagedown/issues/list",
"homepage": "http://code.google.com/p/pagedown/wiki/PageDown"
}

+ 18
- 0
admin/includes/auth.php View File

@ -0,0 +1,18 @@
<div id="login">
<?php if (login()) { ?>
<div class="nav">
<a href="<?php echo config('site.url');?>/admin">Admin</a>
<a href="includes/create_post.php">Create post</a>
<a href="includes/logout.php">Logout</a>
<span class="welcome">Welcome <?php echo $_SESSION['user'];?>!</span>
</div>
<?php include 'includes/post_list.php';?>
<?php } else {?>
<?php include 'includes/login.php';?>
<?php } ?>
</div>

+ 80
- 0
admin/includes/create_post.php View File

@ -0,0 +1,80 @@
<?php
// Change this to your timezone
date_default_timezone_set('Asia/Jakarta');
require '../../system/includes/dispatch.php';
config('source', '../../admin/config.ini');
include '../includes/session.php';
?>
<!DOCTYPE html>
<html>
<head>
<title>Create post</title>
<link rel="stylesheet" type="text/css" href="../resources/style.css" />
<link rel="stylesheet" type="text/css" href="../editor/css/editor.css" />
<script type="text/javascript" src="../editor/js/Markdown.Converter.js"></script>
<script type="text/javascript" src="../editor/js/Markdown.Sanitizer.js"></script>
<script type="text/javascript" src="../editor/js/Markdown.Editor.js"></script>
</head>
<body>
<div class="wrapper-outer">
<div class="wrapper-inner">
<?php if (login()) { ?>
<div class="nav">
<a href="<?php echo config('site.url');?>/admin">Admin</a>
<a href="../includes/logout.php">Logout</a>
<span class="welcome">Welcome <?php echo $_SESSION['user'];?>!</span>
</div>
<?php
if(isset($_POST['submit'])) {
$post_date = date('Y-m-d-H');
$post_tag = $_POST['tag'];
$post_url = $_POST['url'];
$post_content = $_POST['content'];
}
if(!empty($post_tag) && !empty($post_url) && !empty($post_content)) {
$user = $_SESSION['user'];
$filename = $post_date . '_' . $post_tag . '_' . $post_url . '.md';
$dir = '../../content/' . $user. '/blog/';
if(is_dir($dir)) {
file_put_contents($dir . $filename, print_r($post_content, true));
}
else {
mkdir($dir, 0777, true);
file_put_contents($dir . $filename, print_r($post_content, true));
}
header('location: ../index.php');
}
?>
<?php } else {?>
<?php header('location: ../index.php');?>
<?php } ?>
<div class="wmd-panel">
<form method="POST">
Tag: <br><input type="text" name="tag"/><br><br>
Url: <br><input type="text" name="url"/><br><br>
<div id="wmd-button-bar" class="wmd-button-bar"></div>
<textarea id="wmd-input" class="wmd-input" name="content" cols="20" rows="10"></textarea><br/>
<input type="submit" name="submit" value="Publish"/>
</form>
</div>
<div id="wmd-preview" class="wmd-panel wmd-preview"></div>
<script type="text/javascript">
(function () {
var converter = Markdown.getSanitizingConverter();
converter.hooks.chain("preBlockGamut", function (text, rbg) {
return text.replace(/^ {0,3}""" *\n((?:.*?\n)+?) {0,3}""" *$/gm, function (whole, inner) {
return "<blockquote>" + rbg(inner) + "</blockquote>\n";
});
});
var editor = new Markdown.Editor(converter);
editor.run();
})();
</script>
</div>
</div>
</body>
</html>

+ 72
- 0
admin/includes/edit_post.php View File

@ -0,0 +1,72 @@
<?php
// Change this to your timezone
date_default_timezone_set('Asia/Jakarta');
require '../../system/includes/dispatch.php';
config('source', '../../admin/config.ini');
include '../includes/session.php';;
if(isset($_GET['url'])) {
$url = $_GET['url'];
}
else {
header('location: ../index.php');
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Edit post</title>
<link rel="stylesheet" type="text/css" href="../resources/style.css" />
<link rel="stylesheet" type="text/css" href="../editor/css/editor.css" />
<script type="text/javascript" src="../editor/js/Markdown.Converter.js"></script>
<script type="text/javascript" src="../editor/js/Markdown.Sanitizer.js"></script>
<script type="text/javascript" src="../editor/js/Markdown.Editor.js"></script>
</head>
<body>
<div class="wrapper-outer">
<div class="wrapper-inner">
<?php if (login()) { ?>
<div class="nav">
<a href="<?php echo config('site.url');?>/admin">Admin</a>
<a href="../includes/create_post.php">Create post</a>
<a href="../includes/logout.php">Logout</a>
<span class="welcome">Welcome <?php echo $_SESSION['user'];?>!</span>
</div>
<?php } else {?>
<?php header('location: ../index.php');?>
<?php } ?>
<?php
if(isset($_POST['submit'])) {
$post_content = $_POST['content'];
}
if(!empty($post_content)) {
file_put_contents('../'. $url, print_r($post_content, true));
header('location: ../index.php');
}
?>
<div class="wmd-panel">
<form method="POST">
<div id="wmd-button-bar" class="wmd-button-bar"></div>
<textarea id="wmd-input" class="wmd-input" name="content" cols="20" rows="10"><?php echo file_get_contents('../' . $url)?></textarea><br>
<input type="submit" name="submit" value="Submit"/>
</form>
</div>
<div id="wmd-preview" class="wmd-panel wmd-preview"></div>
<script type="text/javascript">
(function () {
var converter = Markdown.getSanitizingConverter();
converter.hooks.chain("preBlockGamut", function (text, rbg) {
return text.replace(/^ {0,3}""" *\n((?:.*?\n)+?) {0,3}""" *$/gm, function (whole, inner) {
return "<blockquote>" + rbg(inner) + "</blockquote>\n";
});
});
var editor = new Markdown.Editor(converter);
editor.run();
})();
</script>
</div>
</div>
</body>
</html>

+ 32
- 0
admin/includes/login.php View File

@ -0,0 +1,32 @@
<?php
if(isset($_POST['submit'])) {
$user = $_POST['user'];
$user_file = 'users/' . $user . '.txt';
$pass = $_POST['password'];
$user_pass = @file_get_contents($user_file);
if(file_exists($user_file)) {
if($pass === $user_pass) {
$_SESSION['user'] = $user;
header('location: index.php');
}
else {
echo 'Username and password not match!';
}
}
else {
echo 'Please create username.txt inside "admin/users" folder and put your password inside it.';
}
}
?>
<p>Login Form</p>
<form method="POST">
User:<br>
<input type="text" name="user"/><br><br>
Pass:<br>
<input type="password" name="password"/><br><br>
<input type="submit" name="submit" value="Login"/>
</form>

+ 9
- 0
admin/includes/logout.php View File

@ -0,0 +1,9 @@
<?php
include 'session.php';
session_destroy();
header('location: ../index.php');
?>

+ 134
- 0
admin/includes/post_list.php View File

@ -0,0 +1,134 @@
<?php
// Get blog post with more info about the path. Sorted by filename.
function admin_get_post(){
static $tmp= array();
static $_cache = array();
if(empty($_cache)){
// Get the names of all the posts
$tmp = glob('../content/*/blog/*.md', GLOB_NOSORT);
foreach($tmp as $file) {
$_cache[] = pathinfo($file);
}
}
usort($_cache, "sortfile");
return $_cache;
}
// usort function. Sort by filename.
function sortfile($a, $b) {
return $a['filename'] == $b['filename'] ? 0 : ( $a['filename'] < $b['filename'] ) ? 1 : -1;
}
// Return blog posts.
function get_posts($posts, $page = 1, $perpage = 0){
if(empty($posts)) {
$posts = admin_get_post();
}
$tmp = array();
// Extract a specific page with results
$posts = array_slice($posts, ($page-1) * $perpage, $perpage);
foreach($posts as $index => $v){
$post = new stdClass;
$filepath = $v['dirname'] . '/' . $v['basename'];
// Extract the date
$arr = explode('_', $filepath);
// Replaced string
$replaced = substr($arr[0], 0,strrpos($arr[0], '/')) . '/';
// Author string
$str = explode('/', $replaced);
$author = $str[count($str)-3];
// The post author + author url
$post->author = $author;
$post->authorurl = site_url() . 'author/' . $author;
// The post date
$post->date = strtotime(str_replace($replaced,'',$arr[0]));
// The archive per day
$post->archive = site_url(). 'archive/' . date('Y-m-d', $post->date) ;
// The post URL
$post->url = site_url().date('Y/m', $post->date).'/'.str_replace('.md','',$arr[2]);
// The post tag
$post->tag = str_replace($replaced,'',$arr[1]);
// The post tag url
$post->tagurl = site_url(). 'tag/' . $arr[1];
$post->file = $filepath;
// Get the contents and convert it to HTML
// $content = file_get_contents($filepath);
// $post->content = $content;
$tmp[] = $post;
}
return $tmp;
}
// Return posts list on profile.
function get_profile($profile, $page, $perpage){
$posts = admin_get_post();
$tmp = array();
foreach ($posts as $index => $v) {
$url = $v['dirname'];
$str = explode('/', $url);
$author = $str[count($str)-2];
if($profile === $author){
$tmp[] = $v;
}
}
if(empty($tmp)) {
echo '<tr><td>No posts found!</td></tr>';
return;
}
return $tmp = get_posts($tmp, $page, $perpage);
}
?>
<table>
<?php
$posts = get_profile($_SESSION['user'], null, null);
if(!empty($posts)) {
foreach($posts as $p) {
echo '<tr>';
echo '<td>' . $p->file . '</td>';
echo '<td><form method="GET" action="includes/edit_post.php"><input type="submit" name="submit" value="Edit"/><input type="hidden" name="url" value="' . $p->file . '"/></form></td>';
echo '</tr>';
}
}
?>
</table>

+ 16
- 0
admin/includes/session.php View File

@ -0,0 +1,16 @@
<?php
session_start();
function login() {
if(isset($_SESSION['user']) && !empty($_SESSION['user'])) {
return true;
}
else {
return false;
}
}
?>

+ 20
- 0
admin/index.php View File

@ -0,0 +1,20 @@
<?php
date_default_timezone_set('Asia/Jakarta');
require '../system/includes/dispatch.php';
config('source', '../admin/config.ini');
include 'includes/session.php';
?>
<!DOCTYPE html>
<html>
<head>
<title>Admin Panel</title>
<link rel="stylesheet" type="text/css" href="resources/style.css" />
</head>
<body>
<div class="wrapper-outer">
<div class="wrapper-inner">
<?php include 'includes/auth.php'; ?>
</div>
</div>
</body>
</html>

+ 42
- 0
admin/resources/style.css View File

@ -0,0 +1,42 @@
body {
font-size: 16px;
font-family: Arial, Verdana;
margin:0 auto;
padding:0;
background: none repeat scroll 0 0 #F9F9F9;
width:100%;
}
.wrapper-outer {
float:left;
position:relative;
width:100%;
}
.wrapper-inner {
margin:0 auto;
width:980px;
}
.nav {
width: 96%;
padding:2%;
}
table {
border: 1px solid #ccc;
}
table td {
border: 1px solid #ccc;
background: #ddd;
}
@media all and (max-width: 980px) {
.wrapper-inner {
margin:0 auto;
width:100%;
}
}

+ 1
- 0
admin/users/.htaccess View File

@ -0,0 +1 @@
deny from all

+ 12
- 5
system/includes/functions.php View File

@ -207,11 +207,16 @@ function get_tag($tag, $page, $perpage){
foreach ($posts as $index => $v) {
$url = $v['filename'];
if( strpos($url, "$tag") !== false){
$str = explode('_', $url);
if($tag === $str[1]){
$tmp[] = $v;
}
}
if(empty($tmp)) {
not_found();
}
return $tmp = get_posts($tmp, $page, $perpage);
}
@ -225,11 +230,16 @@ function get_archive($req, $page, $perpage){
foreach ($posts as $index => $v) {
$url = $v['filename'];
if( strpos($url, "$req") !== false){
$str = explode('_', $url);
if( strpos($str[0], "$req") !== false ){
$tmp[] = $v;
}
}
if(empty($tmp)) {
not_found();
}
return $tmp = get_posts($tmp, $page, $perpage);
}
@ -890,9 +900,6 @@ function get_static_path(){
$tmp = array();
// Create a new instance of the markdown parser
$md = new MarkdownParser();
foreach($posts as $index => $v){
$post = new stdClass;


Loading…
Cancel
Save