├── .babelrc ├── .editorconfig ├── .gitignore ├── README.md ├── build-config.js ├── build ├── browserslist.js ├── gulpfile.js ├── tasks │ ├── build-css.js │ ├── build-example.js │ ├── build-js.js │ ├── build-php.js │ └── clean.js └── utils.js ├── dist ├── README.md ├── example.php └── plugin-manager │ ├── assets │ ├── css │ │ ├── plugin-manager.css │ │ └── plugin-manager.min.css │ └── js │ │ ├── plugin-manager.js │ │ ├── plugin-manager.min.js │ │ ├── plugin-notices.js │ │ └── plugin-notices.min.js │ ├── class-theme-blvd-plugin-manager.php │ ├── class-theme-blvd-plugin-notices.php │ └── class-theme-blvd-plugins.php ├── package-lock.json ├── package.json ├── src ├── assets │ ├── js │ │ ├── modules │ │ │ ├── actions.js │ │ │ ├── queue.js │ │ │ └── vars.js │ │ ├── plugin-manager.js │ │ └── plugin-notices.js │ └── scss │ │ └── plugin-manager.scss ├── class-my-plugin-manager.php ├── class-my-plugin-notices.php ├── class-my-plugins.php ├── example-child-theme.txt ├── example-plugin.txt └── example-theme.txt └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | "browsers": [ 6 | ">= 1%", 7 | "last 1 major version", 8 | "not dead", 9 | "Chrome >= 45", 10 | "Firefox >= 38", 11 | "Edge >= 12", 12 | "Explorer >= 11", 13 | "iOS >= 9", 14 | "Safari >= 9", 15 | "Android >= 4.4", 16 | "Opera >= 30" 17 | ] 18 | }, 19 | "modules": false 20 | }] 21 | ], 22 | "plugins": ["transform-object-rest-spread"] 23 | } 24 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | indent_style = space 9 | indent_size = 2 10 | max_line_length = 100 11 | quote_type = single 12 | 13 | [**/*.php] 14 | indent_style = tab 15 | indent_size = 4 16 | max_line_length = 200 17 | 18 | [*.{json,babelrc}] 19 | max_line_length = 200 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .sass-cache 3 | npm-debug.log 4 | node_modules 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # {{my}}_Plugin_Manager 2 | 3 | [My Plugin Manager](http://mypluginmanager.com) is a drop-in script for your WordPress theme or plugin, which gives your users an interface to manage plugins you suggest be used with your product. 4 | 5 | ![My Plugin Manager Preview](http://mypluginmanager.com/assets/img/screenshots/intro.gif "My Plugin Manager Preview") 6 | 7 | ## Download and Usage 8 | 9 | Please do not copy the files from this repository directly into your WordPress project. Make sure to use one of the methods below to generate the script for your specific project. 10 | 11 | Our goal is to provide you with a drop-in script that complies as closely to WordPress coding standards and best practices, as possible. So by generating a custom build for your project, the following will happen: 12 | 13 | * Prefix all PHP class names to match your theme or plugin name. 14 | * Name all class files to correctly correspond to the name of those PHP classes. 15 | * Insert your unique namespacing key for anything submitted to the database. 16 | * Insert the localization text domain that matches your theme or plugin. 17 | * If the "plugin" usage type is selected, `add_theme_page()` will be substituted for `add_submenu_page()` (not allowed in themes). 18 | * Give you an example code snippet to get started, which is customized to your project. 19 | 20 | ### Method 1: Use the Online Generator 21 | 22 | The easiest way to incorporate your new plugin manager into your WordPress theme or plugin is to generate a custom instance for your project at: 23 | 24 | http://mypluginmanager.com#download 25 | 26 | After you've submitted the form, your plugin manager download will start and you'll be shown instructions specific to your project to get started. 27 | 28 | *Note: Curious how mypluginmanager.com is built? It's an open source PHP web app. [Check it out here.](https://github.com/themeblvd/my-plugin-manager-site)* 29 | 30 | ### Method 2: Clone the Repo 31 | 32 | For using this repository to generate a custom build, you'll need to have [NodeJS](https://nodejs.org) and [NPM](https://www.npmjs.com/get-npm) installed on your computer. 33 | 34 | 1. Clone this repository to your local computer. 35 | 2. Edit the variables in `build-config.js` to match your project. 36 | 4. Within the cloned repo, run `npm install` to install all of dependencies from your terminal. 37 | 5. Then run `npm run build` to generate a custom build specific to your project, using data from the `build-config.js` you edited. 38 | 6. Within the `/dist` directory, find the `plugin-manager` directory that was generated and copy it to the root of your WordPress theme or plugin. 39 | 7. Also within the `/dist` directory you'll see an `example.php` file. You can copy the contents of this file into your theme or plugin's PHP code as a starting point to implement the script. 40 | 41 | ## More Information 42 | 43 | * [About](http://mypluginmanager.com/about) 44 | * [Frequently Asked Questions](http://mypluginmanager.com/faq) 45 | * [Documentation](http://mypluginmanager.com/docs) 46 | 47 | ## Copyright and License 48 | 49 | Code and documentation copyright 2011-2017 [Jason Bobich](http://jasonbobich.com) and [Theme Blvd](http://themeblvd.com). Code released under the [GPLv2 or later](http://www.gnu.org/licenses/gpl-2.0.html) license. 50 | -------------------------------------------------------------------------------- /build-config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Build Data 3 | * 4 | * The following variables are used to generate 5 | * the build. 6 | * 7 | * The default values you see below are how we 8 | * integrate this script into Theme Blvd themes. 9 | * 10 | * If you're using a cloned version of this repo, 11 | * to generate an instance for your theme or plugin, 12 | * you'll want to change the values below. 13 | * 14 | * Or, you can also generate a custom instance for 15 | * your theme or plugin at mypluginmanager.com. 16 | */ 17 | 18 | /** 19 | * Context of how you're using the script. 20 | * 21 | * Value should be equal to one of the following: 22 | * 1. `theme` - Using with standard theme. 23 | * 2. `child-theme` - Using with child theme. 24 | * 3. `plugin` - Using with plugin. 25 | * 26 | * @type {string} 27 | */ 28 | const usage = 'theme'; 29 | 30 | /** 31 | * Name of your theme or plugin. 32 | * 33 | * This should match the name in your theme's style.css 34 | * or the name in your main plugin file. 35 | * 36 | * @type {string} 37 | */ 38 | const name = 'Theme Blvd'; 39 | 40 | /** 41 | * Namespace value you're using for your PHP functions, 42 | * like `themeblvd` or `theme_blvd` 43 | * 44 | * @type {string} 45 | */ 46 | const namespace = 'themeblvd'; 47 | 48 | /** 49 | * Text domain slug your plugin or theme uses, like 50 | * `themeblvd` or `theme-blvd`. 51 | * 52 | * This is the slug used for your WordPress localization. 53 | * It usually matches the name of your theme or plugin's 54 | * directory. 55 | * 56 | * Note: What's up with the weird '@@text-domain' default 57 | * value? -- In the Theme Blvd WordPress framework we have 58 | * a build system that replaces that value throughout all 59 | * framework PHP code with the proper value cooresponding 60 | * to the current theme. 61 | * 62 | * @type {string} 63 | */ 64 | const textDomain = '@@text-domain'; 65 | 66 | module.exports = { usage, name, namespace, textDomain }; 67 | -------------------------------------------------------------------------------- /build/browserslist.js: -------------------------------------------------------------------------------- 1 | const browserslist = [ 2 | '>= 1%', 3 | 'last 1 major version', 4 | 'not dead', 5 | 'Chrome >= 45', 6 | 'Firefox >= 38', 7 | 'Edge >= 12', 8 | 'Explorer >= 11', 9 | 'iOS >= 9', 10 | 'Safari >= 9', 11 | 'Android >= 4.4', 12 | 'Opera >= 30' 13 | ]; 14 | 15 | module.exports = browserslist; 16 | -------------------------------------------------------------------------------- /build/gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const buildJs = require('./tasks/build-js'); 3 | 4 | gulp.task('clean', require('./tasks/clean')); 5 | 6 | gulp.task('build-example', require('./tasks/build-example')); 7 | 8 | gulp.task('build-php', require('./tasks/build-php')); 9 | 10 | gulp.task('build-css', require('./tasks/build-css')); 11 | 12 | gulp.task('build-js-notices', () => buildJs('notices')); 13 | 14 | gulp.task('build-js-manager', () => buildJs('manager')); 15 | 16 | gulp.task('build-js', ['build-js-notices', 'build-js-manager']); 17 | 18 | gulp.task('default', ['clean', 'build-example', 'build-php', 'build-css', 'build-js']); 19 | -------------------------------------------------------------------------------- /build/tasks/build-css.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const sass = require('gulp-sass'); 3 | const autoprefixer = require('gulp-autoprefixer'); 4 | const browserslist = require('../browserslist'); 5 | const minifycss = require('gulp-clean-css'); 6 | const rename = require('gulp-rename'); 7 | 8 | function buildCss() { 9 | return gulp 10 | .src('../src/assets/scss/plugin-manager.scss') 11 | .pipe(sass({ outputStyle: 'expanded' }).on('error', sass.logError)) 12 | .pipe(autoprefixer({ browsers: browserslist })) 13 | .pipe(gulp.dest('../dist/plugin-manager/assets/css')) 14 | .pipe(minifycss()) 15 | .pipe(rename({ suffix: '.min' })) 16 | .pipe(gulp.dest('../dist/plugin-manager/assets/css')); 17 | } 18 | 19 | module.exports = buildCss; 20 | -------------------------------------------------------------------------------- /build/tasks/build-example.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const replace = require('gulp-replace'); 3 | const rename = require('gulp-rename'); 4 | const { classPrefix, classFilePrefix } = require('../utils'); 5 | const { usage, name, namespace, textDomain } = require('../../build-config'); 6 | 7 | function buildExample() { 8 | gulp 9 | .src(`../src/example-${usage}.txt`) 10 | .pipe(replace('{{name}}', name)) 11 | .pipe(replace('{{text-domain}}', textDomain)) 12 | .pipe(replace('{{my}}', namespace)) 13 | .pipe(replace('{{My}}', classPrefix(name))) 14 | .pipe(replace('{{class-my-}}', classFilePrefix(name))) 15 | .pipe(rename('example.php')) 16 | .pipe(gulp.dest('../dist')); 17 | } 18 | 19 | module.exports = buildExample; 20 | -------------------------------------------------------------------------------- /build/tasks/build-js.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | const webpackStream = require('webpack-stream'); 5 | const webpackConfig = require('../../webpack.config'); 6 | 7 | function buildJs(script) { 8 | const entry = path.resolve(__dirname, '../../src') + `/assets/js/plugin-${script}.js`; 9 | 10 | return gulp 11 | .src(entry) // For Gulp reference only, actual entry file pulled from Webpack config. 12 | .pipe(webpackStream(webpackConfig({ entry, script }), webpack)) 13 | .pipe(gulp.dest('../dist')); // Replaces path in typical Webpack output object. 14 | } 15 | 16 | module.exports = buildJs; 17 | -------------------------------------------------------------------------------- /build/tasks/build-php.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const replace = require('gulp-replace'); 3 | const replaceName = require('gulp-replace-name'); 4 | const { classPrefix, classFilePrefix } = require('../utils'); 5 | const { usage, name, namespace, textDomain } = require('../../build-config'); 6 | 7 | function buildPhp() { 8 | const menuSlug = { 9 | find: usage === 'plugin' ? 'themes.php' : '', 10 | replace: usage === 'plugin' ? 'plugins.php' : '' 11 | }; 12 | 13 | const addMenu = { 14 | find: usage === 'plugin' ? 'add_theme_page(' : '', 15 | replace: usage === 'plugin' ? "add_submenu_page(\n\t\t\t\t$this->args['parent_slug']," : '' 16 | }; 17 | 18 | return gulp 19 | .src('../src/*.php') 20 | .pipe(replace('my-text-domain', textDomain)) 21 | .pipe(replace('my_namespace', namespace)) 22 | .pipe(replace('_My', classPrefix(name))) 23 | .pipe(replace(menuSlug.find, menuSlug.replace)) 24 | .pipe(replace(addMenu.find, addMenu.replace)) 25 | .pipe(replace('class-my-', classFilePrefix(name))) 26 | .pipe(replaceName(/class-my-/g, classFilePrefix(name))) 27 | .pipe(gulp.dest('../dist/plugin-manager')); 28 | } 29 | 30 | module.exports = buildPhp; 31 | -------------------------------------------------------------------------------- /build/tasks/clean.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const del = require('del'); 3 | 4 | function clean() { 5 | return del(['../dist/*', '!../dist/README.md'], { force: true }); 6 | } 7 | 8 | module.exports = clean; 9 | -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate PHP class prefix. 3 | * 4 | * @param {string} name WordPress theme or plugin name. 5 | * @return {string} 6 | */ 7 | function classPrefix(name) { 8 | return name.replace(/ /gi, '_'); 9 | } 10 | 11 | /** 12 | * Generate PHP class file prefix. 13 | * 14 | * @param {string} name WordPress theme or plugin name. 15 | * @return {string} 16 | */ 17 | function classFilePrefix(name) { 18 | let prefix = classPrefix(name); 19 | prefix = prefix.toLowerCase().replace(/_/gi, '-'); 20 | prefix = `class-${prefix}-`; 21 | return prefix; 22 | } 23 | 24 | module.exports = { classPrefix, classFilePrefix }; 25 | -------------------------------------------------------------------------------- /dist/README.md: -------------------------------------------------------------------------------- 1 | # An Example 2 | 3 | This directory contains an example of what can be generated from the `/src` directory. To generate your own plugin manager, please go to: 4 | 5 | http://mypluginmanager.com/#download 6 | 7 | This example was generated with the following data: 8 | 9 | * Usage Type: `theme` — Could be `theme`, `child-theme` or `plugin`. 10 | * Theme Name: `Theme Blvd` 11 | * Namespace: `themeblvd` 12 | * Text Domain: `@@text-domain` — Would normally be like `themeblvd` or `theme-blvd`. 13 | -------------------------------------------------------------------------------- /dist/example.php: -------------------------------------------------------------------------------- 1 | /** 2 | * Setup suggested plugin system. 3 | * 4 | * Include the Theme_Blvd_Plugin_Manager class and add 5 | * an interface for users to to manage suggested 6 | * plugins. 7 | * 8 | * @since x.x.x 9 | * 10 | * @see Theme_Blvd_Plugin_Manager 11 | * @link http://mypluginmanager.com 12 | */ 13 | function themeblvd_plugin_manager() { 14 | 15 | if ( ! is_admin() ) { 16 | return; 17 | } 18 | 19 | /** 20 | * Include plugin manager class. 21 | * 22 | * No other includes are needed. The Theme_Blvd_Plugin_Manager 23 | * class will handle including any other files needed. 24 | * 25 | * If you want to move the "plugin-manager" directory to 26 | * a different location within your theme, that's totally 27 | * fine; just make sure you adjust this include path to 28 | * the plugin manager class accordingly. 29 | */ 30 | require_once( get_parent_theme_file_path( '/plugin-manager/class-theme-blvd-plugin-manager.php' ) ); 31 | 32 | /* 33 | * Setup suggested plugins. 34 | * 35 | * It's a good idea to have a filter applied to this so your 36 | * loyal users running child themes have a way to easily modify 37 | * which plugins show as suggested for the site they're setting 38 | * up for a client. 39 | */ 40 | $plugins = apply_filters( 'themeblvd_plugins', array( 41 | array( 42 | 'name' => 'Easy Digital Downloads', 43 | 'slug' => 'easy-digital-downloads', 44 | 'version' => '2.8+', 45 | ), 46 | array( 47 | 'name' => 'JetPack', 48 | 'slug' => 'jetpack', 49 | 'version' => '5.0+', 50 | ), 51 | array( 52 | 'name' => 'Gravity Forms', 53 | 'slug' => 'gravityforms', 54 | 'version' => '2.2+', 55 | 'url' => 'http://www.gravityforms.com', // Only for non wp.org plugins. 56 | ), 57 | )); 58 | 59 | /* 60 | * Setup optional arguments for plugin manager interface. 61 | * 62 | * See the set_args() method of the Theme_Blvd_Plugin_Manager 63 | * class for full documentation on what you can pass in here. 64 | */ 65 | $args = array( 66 | 'page_title' => __( 'Suggested Plugins', '@@text-domain' ), 67 | 'menu_slug' => '@@text-domain-suggested-plugins', 68 | ); 69 | 70 | /* 71 | * Create plugin manager object, passing in the suggested 72 | * plugins and optional arguments. 73 | */ 74 | $manager = new Theme_Blvd_Plugin_Manager( $plugins, $args ); 75 | 76 | } 77 | add_action( 'after_setup_theme', 'themeblvd_plugin_manager' ); 78 | -------------------------------------------------------------------------------- /dist/plugin-manager/assets/css/plugin-manager.css: -------------------------------------------------------------------------------- 1 | #suggested-plugins .column-compatible-version, 2 | #suggested-plugins .column-installed-version, 3 | #suggested-plugins .column-status { 4 | width: 180px; 5 | } 6 | 7 | #suggested-plugins .incompatible td, 8 | #suggested-plugins .incompatible th { 9 | background-color: #fff8e5; 10 | } 11 | 12 | #suggested-plugins .incompatible .column-suggested-version, 13 | #suggested-plugins .incompatible .column-installed-version, 14 | #suggested-plugins .incompatible .column-status { 15 | font-weight: bold; 16 | } 17 | 18 | #suggested-plugins .incompatible th.check-column { 19 | border-left: 4px solid #ffb900; 20 | } 21 | 22 | #suggested-plugins td, 23 | #suggested-plugins th { 24 | transition: background-color 0.75s ease-in-out; 25 | } 26 | 27 | #suggested-plugins .install-success td, 28 | #suggested-plugins .install-success th, 29 | #suggested-plugins .update-success td, 30 | #suggested-plugins .update-success th { 31 | background-color: #ecf7ed; 32 | } 33 | 34 | #suggested-plugins .delete-success td, 35 | #suggested-plugins .delete-success th { 36 | background-color: #fbeaea; 37 | } 38 | 39 | #suggested-plugins .row-has-notice td, 40 | #suggested-plugins .row-has-notice th { 41 | box-shadow: none; 42 | } 43 | 44 | #suggested-plugins .row-notice td { 45 | box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.1); 46 | overflow: hidden; 47 | padding: 0; 48 | } 49 | 50 | #suggested-plugins .row-notice .notice { 51 | margin: 0 20px 15px 40px; 52 | } 53 | 54 | #suggested-plugins .row-notice .notice p:before { 55 | content: '\f534'; 56 | } 57 | 58 | #suggested-plugins .row-actions { 59 | line-height: 25px; 60 | } 61 | 62 | #suggested-plugins .updating-message .row-actions:before { 63 | content: '\f463'; 64 | -webkit-animation: rotation 2s infinite linear; 65 | animation: rotation 2s infinite linear; 66 | color: #f56e28; 67 | display: inline-block; 68 | font: normal 20px/1 'dashicons'; 69 | -webkit-font-smoothing: antialiased; 70 | -moz-osx-font-smoothing: grayscale; 71 | margin-right: 2px; 72 | vertical-align: top; 73 | } 74 | 75 | #suggested-plugins .row-actions .no-link { 76 | color: #666; 77 | } 78 | 79 | .rtl #suggested-plugins .incompatible th.check-column { 80 | border-left: none; 81 | border-right: 4px solid #ffb900; 82 | } 83 | 84 | .rtl #suggested-plugins .row-notice .notice { 85 | margin-right: 40px; 86 | margin-left: 20px; 87 | } 88 | 89 | .rtl #suggested-plugins .updating-message .row-actions:before { 90 | margin-right: 0; 91 | margin-left: 2px; 92 | } 93 | -------------------------------------------------------------------------------- /dist/plugin-manager/assets/css/plugin-manager.min.css: -------------------------------------------------------------------------------- 1 | #suggested-plugins .column-compatible-version,#suggested-plugins .column-installed-version,#suggested-plugins .column-status{width:180px}#suggested-plugins .incompatible td,#suggested-plugins .incompatible th{background-color:#fff8e5}#suggested-plugins .incompatible .column-installed-version,#suggested-plugins .incompatible .column-status,#suggested-plugins .incompatible .column-suggested-version{font-weight:700}#suggested-plugins .incompatible th.check-column{border-left:4px solid #ffb900}#suggested-plugins td,#suggested-plugins th{transition:background-color .75s ease-in-out}#suggested-plugins .install-success td,#suggested-plugins .install-success th,#suggested-plugins .update-success td,#suggested-plugins .update-success th{background-color:#ecf7ed}#suggested-plugins .delete-success td,#suggested-plugins .delete-success th{background-color:#fbeaea}#suggested-plugins .row-has-notice td,#suggested-plugins .row-has-notice th{box-shadow:none}#suggested-plugins .row-notice td{box-shadow:inset 0 -1px 0 rgba(0,0,0,.1);overflow:hidden;padding:0}#suggested-plugins .row-notice .notice{margin:0 20px 15px 40px}#suggested-plugins .row-notice .notice p:before{content:'\f534'}#suggested-plugins .row-actions{line-height:25px}#suggested-plugins .updating-message .row-actions:before{content:'\f463';-webkit-animation:rotation 2s infinite linear;animation:rotation 2s infinite linear;color:#f56e28;display:inline-block;font:normal 20px/1 dashicons;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;margin-right:2px;vertical-align:top}#suggested-plugins .row-actions .no-link{color:#666}.rtl #suggested-plugins .incompatible th.check-column{border-left:none;border-right:4px solid #ffb900}.rtl #suggested-plugins .row-notice .notice{margin-right:40px;margin-left:20px}.rtl #suggested-plugins .updating-message .row-actions:before{margin-right:0;margin-left:2px} -------------------------------------------------------------------------------- /dist/plugin-manager/assets/js/plugin-manager.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | /******/ 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | /******/ 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) { 10 | /******/ return installedModules[moduleId].exports; 11 | /******/ } 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ i: moduleId, 15 | /******/ l: false, 16 | /******/ exports: {} 17 | /******/ }; 18 | /******/ 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | /******/ 22 | /******/ // Flag the module as loaded 23 | /******/ module.l = true; 24 | /******/ 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | /******/ 29 | /******/ 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | /******/ 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | /******/ 36 | /******/ // define getter function for harmony exports 37 | /******/ __webpack_require__.d = function(exports, name, getter) { 38 | /******/ if(!__webpack_require__.o(exports, name)) { 39 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); 40 | /******/ } 41 | /******/ }; 42 | /******/ 43 | /******/ // define __esModule on exports 44 | /******/ __webpack_require__.r = function(exports) { 45 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 46 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 47 | /******/ } 48 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 49 | /******/ }; 50 | /******/ 51 | /******/ // create a fake namespace object 52 | /******/ // mode & 1: value is a module id, require it 53 | /******/ // mode & 2: merge all properties of value into the ns 54 | /******/ // mode & 4: return value when already ns object 55 | /******/ // mode & 8|1: behave like require 56 | /******/ __webpack_require__.t = function(value, mode) { 57 | /******/ if(mode & 1) value = __webpack_require__(value); 58 | /******/ if(mode & 8) return value; 59 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; 60 | /******/ var ns = Object.create(null); 61 | /******/ __webpack_require__.r(ns); 62 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); 63 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); 64 | /******/ return ns; 65 | /******/ }; 66 | /******/ 67 | /******/ // getDefaultExport function for compatibility with non-harmony modules 68 | /******/ __webpack_require__.n = function(module) { 69 | /******/ var getter = module && module.__esModule ? 70 | /******/ function getDefault() { return module['default']; } : 71 | /******/ function getModuleExports() { return module; }; 72 | /******/ __webpack_require__.d(getter, 'a', getter); 73 | /******/ return getter; 74 | /******/ }; 75 | /******/ 76 | /******/ // Object.prototype.hasOwnProperty.call 77 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 78 | /******/ 79 | /******/ // __webpack_public_path__ 80 | /******/ __webpack_require__.p = ""; 81 | /******/ 82 | /******/ 83 | /******/ // Load entry module and return exports 84 | /******/ return __webpack_require__(__webpack_require__.s = 4); 85 | /******/ }) 86 | /************************************************************************/ 87 | /******/ ([ 88 | /* 0 */ 89 | /***/ (function(module, exports) { 90 | 91 | module.exports = jQuery; 92 | 93 | /***/ }), 94 | /* 1 */ 95 | /***/ (function(module, exports) { 96 | 97 | module.exports = wp; 98 | 99 | /***/ }), 100 | /* 2 */ 101 | /***/ (function(module, exports) { 102 | 103 | /* 104 | * Build Data 105 | * 106 | * The following variables are used to generate 107 | * the build. 108 | * 109 | * The default values you see below are how we 110 | * integrate this script into Theme Blvd themes. 111 | * 112 | * If you're using a cloned version of this repo, 113 | * to generate an instance for your theme or plugin, 114 | * you'll want to change the values below. 115 | * 116 | * Or, you can also generate a custom instance for 117 | * your theme or plugin at mypluginmanager.com. 118 | */ 119 | 120 | /** 121 | * Context of how you're using the script. 122 | * 123 | * Value should be equal to one of the following: 124 | * 1. `theme` - Using with standard theme. 125 | * 2. `child-theme` - Using with child theme. 126 | * 3. `plugin` - Using with plugin. 127 | * 128 | * @type {string} 129 | */ 130 | var usage = 'theme'; 131 | 132 | /** 133 | * Name of your theme or plugin. 134 | * 135 | * This should match the name in your theme's style.css 136 | * or the name in your main plugin file. 137 | * 138 | * @type {string} 139 | */ 140 | var name = 'Theme Blvd'; 141 | 142 | /** 143 | * Namespace value you're using for your PHP functions, 144 | * like `themeblvd` or `theme_blvd` 145 | * 146 | * @type {string} 147 | */ 148 | var namespace = 'themeblvd'; 149 | 150 | /** 151 | * Text domain slug your plugin or theme uses, like 152 | * `themeblvd` or `theme-blvd`. 153 | * 154 | * This is the slug used for your WordPress localization. 155 | * It usually matches the name of your theme or plugin's 156 | * directory. 157 | * 158 | * Note: What's up with the weird '@@text-domain' default 159 | * value? -- In the Theme Blvd WordPress framework we have 160 | * a build system that replaces that value throughout all 161 | * framework PHP code with the proper value cooresponding 162 | * to the current theme. 163 | * 164 | * @type {string} 165 | */ 166 | var textDomain = '@@text-domain'; 167 | 168 | module.exports = { usage: usage, name: name, namespace: namespace, textDomain: textDomain }; 169 | 170 | /***/ }), 171 | /* 3 */ 172 | /***/ (function(module, exports) { 173 | 174 | module.exports = pluginManagerSettings; 175 | 176 | /***/ }), 177 | /* 4 */ 178 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 179 | 180 | "use strict"; 181 | __webpack_require__.r(__webpack_exports__); 182 | 183 | // EXTERNAL MODULE: external "jQuery" 184 | var external_jQuery_ = __webpack_require__(0); 185 | var external_jQuery_default = /*#__PURE__*/__webpack_require__.n(external_jQuery_); 186 | 187 | // EXTERNAL MODULE: external "wp" 188 | var external_wp_ = __webpack_require__(1); 189 | 190 | // EXTERNAL MODULE: .-config.js 191 | var _config = __webpack_require__(2); 192 | 193 | // EXTERNAL MODULE: external "pluginManagerSettings" 194 | var external_pluginManagerSettings_ = __webpack_require__(3); 195 | var external_pluginManagerSettings_default = /*#__PURE__*/__webpack_require__.n(external_pluginManagerSettings_); 196 | 197 | // CONCATENATED MODULE: ../src/assets/js/modules/vars.js 198 | 199 | 200 | 201 | 202 | /** 203 | * Cache the jQuery document. 204 | * 205 | * @since 1.0.0 206 | * 207 | * @type {object} 208 | */ 209 | var $document = external_jQuery_default()(document); 210 | 211 | /** 212 | * Localized strings from WordPress's updates 213 | * script, merged with any added by the 214 | * plugin manager. 215 | * 216 | * @since 1.0.0 217 | * 218 | * @type {object} 219 | */ 220 | var l10n = external_jQuery_default.a.extend(external_pluginManagerSettings_default.a, external_wp_["updates"].l10n); 221 | // CONCATENATED MODULE: ../src/assets/js/modules/actions.js 222 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | /** 231 | * Keep track of the current action 232 | * taking place. 233 | * 234 | * @type {string} 235 | */ 236 | var currentAction = null; 237 | 238 | /** 239 | * Set the current action. 240 | * 241 | * @param {string} action Action slug or null. 242 | */ 243 | var setCurrentAction = function setCurrentAction(action) { 244 | currentAction = action; 245 | }; 246 | 247 | /** 248 | * Get the current action. 249 | * 250 | * @return {string} Current action. 251 | */ 252 | var getCurrentAction = function getCurrentAction() { 253 | return currentAction; 254 | }; 255 | 256 | /** 257 | * Sends an Ajax request to the server to install a plugin. 258 | * 259 | * @since 1.0.0 260 | * 261 | * @param {object} args Arguments. 262 | * @param {string} args.slug Plugin slug. 263 | * @param {refreshRow=} args.success Optional. Success callback. 264 | * @param {refreshRow=} args.error Optional. Error callback. 265 | * @return {$.promise} A jQuery promise that represents the request, 266 | * decorated with an abort() method. 267 | */ 268 | var actions_installPlugin = function installPlugin(args) { 269 | args = _extends({}, args, { 270 | success: actions_refreshRow, 271 | error: actions_refreshRow 272 | }); 273 | 274 | $document.trigger('wp-plugin-installing', args); 275 | 276 | return external_wp_["updates"].ajax('install-plugin', args); 277 | }; 278 | 279 | /** 280 | * Sends an Ajax request to the server to update a plugin. 281 | * 282 | * @since 1.0.0 283 | * 284 | * @param {object} args Arguments. 285 | * @param {string} args.plugin Plugin basename. 286 | * @param {string} args.slug Plugin slug. 287 | * @param {refreshRow=} args.success Optional. Success callback. 288 | * @param {refreshRow=} args.error Optional. Error callback. 289 | * @return {$.promise} A jQuery promise that represents the request, 290 | * decorated with an abort() method. 291 | */ 292 | var actions_updatePlugin = function updatePlugin(args) { 293 | args = _extends({}, args, { 294 | success: actions_refreshRow, 295 | error: actions_refreshRow 296 | }); 297 | 298 | $document.trigger('wp-plugin-updating', args); 299 | 300 | return external_wp_["updates"].ajax('update-plugin', args); 301 | }; 302 | 303 | /** 304 | * Sends an Ajax request to the server to delete a plugin. 305 | * 306 | * @since 1.0.0 307 | * 308 | * @param {object} args Arguments. 309 | * @param {string} args.plugin Plugin basename. 310 | * @param {string} args.slug Plugin slug. 311 | * @param {refreshRow=} args.success Optional. Success callback. 312 | * @param {refreshRow=} args.error Optional. Error callback. 313 | * @return {$.promise} A jQuery promise that represents the request, 314 | * decorated with an abort() method. 315 | */ 316 | var actions_deletePlugin = function deletePlugin(args) { 317 | args = _extends({}, args, { 318 | success: actions_refreshRow, 319 | error: actions_refreshRow 320 | }); 321 | 322 | $document.trigger('wp-plugin-deleting', args); 323 | 324 | return external_wp_["updates"].ajax('delete-plugin', args); 325 | }; 326 | 327 | /** 328 | * If the state of a plugin has been succesfully changed, refresh 329 | * its table row in the UI. This includes handling success or error. 330 | * 331 | * @since 1.0.0 332 | * 333 | * @param {object} response Response from the server. 334 | * @param {string} response.slug Slug of the plugin that was deleted. 335 | * @param {string} response.plugin Base name of the plugin that was deleted (only 'delete-plugin'). 336 | * @param {string} response.pluginName Name of the plugin that was deleted. 337 | * @param {string} response.activateUrl URL to activate the just installed plugin (only 'install-plugin'). 338 | * @param {string} response.oldVersion Old version of the plugin (only 'update-plugin'). 339 | * @param {string} response.newVersion New version of the plugin (only 'update-plugin'). 340 | */ 341 | var actions_refreshRow = function refreshRow(response) { 342 | var args = { 343 | _ajax_nonce: external_wp_["updates"].ajaxNonce, 344 | action: _config["namespace"] + '-row-refresh', 345 | prev_action: getCurrentAction(), 346 | slug: response.slug, 347 | namespace: _config["namespace"], 348 | error: null, 349 | error_level: 'error' // `error` or `warning` 350 | }; 351 | 352 | // Handle error message 353 | if (response.errorMessage) { 354 | if ('update-plugin' === args.prev_action && response.debug) { 355 | args.error = response.debug[0]; // More helpful message. 356 | args.error_level = 'warning'; 357 | } else { 358 | args.error = response.errorMessage; 359 | } 360 | 361 | if (response.errorLevel) { 362 | args.error_level = response.errorLevel; 363 | } 364 | } 365 | 366 | var $old_row = external_jQuery_default()('tr[data-slug="' + args.slug + '"]'); 367 | 368 | // Refresh row from plugin manager. 369 | external_jQuery_default.a.post(ajaxurl, args, function (response) { 370 | external_jQuery_default()('#setting-error-plugin-manager-error').remove(); // Remove any lingering notices in page header, from previously finished actions. 371 | 372 | external_jQuery_default()('.' + args.slug + '-notice').remove(); // Remove notice on current row, if exists. 373 | 374 | external_jQuery_default()('tr[data-slug="' + args.slug + '"]').replaceWith(response); 375 | 376 | var $new_row = external_jQuery_default()('tr[data-slug="' + args.slug + '"]'); 377 | 378 | if ($new_row.hasClass('ajax-success')) { 379 | setTimeout(function () { 380 | $new_row.removeClass('install-success update-success delete-success'); 381 | }, 200); 382 | } 383 | 384 | // Clear current action. 385 | setCurrentAction(null); 386 | 387 | // Continue processing any bulk actions, if they exist. 388 | queue_processQueue(); 389 | }); 390 | }; 391 | 392 | /** 393 | * Exports 394 | */ 395 | 396 | // CONCATENATED MODULE: ../src/assets/js/modules/queue.js 397 | 398 | 399 | 400 | /** 401 | * Queue of plugins to bulk process. 402 | * 403 | * @since 1.0.0 404 | * 405 | * @type {array} 406 | */ 407 | var queue = []; 408 | 409 | /** 410 | * When performing bulk actions, this will provide a way to 411 | * iterate through the queue of plugins synchronously to 412 | * take action on. 413 | * 414 | * @since 1.0.0 415 | */ 416 | var queue_processQueue = function processQueue() { 417 | if (!queue.length) { 418 | return; 419 | } 420 | 421 | var current = queue[0]; 422 | var action = current.action; 423 | var $row = current.row; 424 | 425 | queue.shift(); 426 | 427 | if ('install-selected' === action) { 428 | $row.addClass('updating-message').find('.row-actions').text(l10n.installing); 429 | 430 | setCurrentAction('install-plugin'); 431 | 432 | if ('third-party' === $row.data('source')) { 433 | /* 434 | * We can only install plugins from wordpress.org. 435 | * So, we'll just bypass to refreshing the row 436 | * with an error message. 437 | */ 438 | actions_refreshRow({ 439 | slug: current.slug, 440 | plugin: current.plugin, 441 | errorMessage: l10n.thirdParty, 442 | errorLevel: 'warning' 443 | }); 444 | } else { 445 | actions_installPlugin({ 446 | slug: current.slug 447 | }); 448 | } 449 | } else if ('update-selected' === action) { 450 | $row.addClass('updating-message').find('.row-actions').text(l10n.updating); 451 | 452 | setCurrentAction('update-plugin'); 453 | 454 | if ('not-installed' === $row.data('status')) { 455 | /* 456 | * We can't update a plugin that's not installed. So, 457 | * we'll just bypass to refreshing the row with an 458 | * error message. 459 | */ 460 | actions_refreshRow({ 461 | slug: current.slug, 462 | plugin: current.plugin, 463 | errorMessage: l10n.notInstalled, 464 | errorLevel: 'warning' 465 | }); 466 | } else { 467 | actions_updatePlugin({ 468 | slug: current.slug, 469 | plugin: current.plugin 470 | }); 471 | } 472 | } else if ('delete-selected' === action) { 473 | if ('not-installed' == $row.data('status')) { 474 | /* 475 | * If we're deleting a plugin that's not actually 476 | * installed, we'll just refresh the row to clear 477 | * any previous notices. No error message needed. 478 | * 479 | * Note: By not setting currentAction, 480 | * there's no visual success or error animation 481 | * added to the refreshed row. 482 | */ 483 | actions_refreshRow({ 484 | slug: current.slug, 485 | plugin: current.plugin 486 | }); 487 | } else { 488 | $row.addClass('updating-message').find('.row-actions').text(l10n.deleting); 489 | 490 | setCurrentAction('delete-plugin'); 491 | 492 | actions_deletePlugin({ 493 | slug: current.slug, 494 | plugin: current.plugin 495 | }); 496 | } 497 | } 498 | }; 499 | 500 | /** 501 | * Exports 502 | */ 503 | 504 | // CONCATENATED MODULE: ../src/assets/js/plugin-manager.js 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | $document.ready(function ($) { 513 | var $page = $('#suggested-plugins'); 514 | 515 | /** 516 | * Handles installing a plugin from clicking `.install-now` 517 | * link. 518 | * 519 | * @since 1.0.0 520 | * 521 | * @param {Event} event Event interface. 522 | */ 523 | $page.on('click', '.install-now', function (event) { 524 | event.preventDefault(); 525 | 526 | var $link = $(event.target); 527 | var $row = $link.closest('tr'); 528 | 529 | if ($row.hasClass('updating-message')) { 530 | return; 531 | } 532 | 533 | $row.addClass('updating-message').find('.row-actions').text(l10n.installing); 534 | 535 | setCurrentAction('install-plugin'); 536 | 537 | actions_installPlugin({ 538 | slug: $row.data('slug') 539 | }); 540 | }); 541 | 542 | /** 543 | * Handles updating a plugin from clicking `.update-now` 544 | * link. 545 | * 546 | * @since 1.0.0 547 | * 548 | * @param {Event} event Event interface. 549 | */ 550 | $page.on('click', '.update-now', function (event) { 551 | event.preventDefault(); 552 | 553 | var $link = $(event.target); 554 | var $row = $link.closest('tr'); 555 | 556 | if ($row.hasClass('updating-message')) { 557 | return; 558 | } 559 | 560 | $row.addClass('updating-message').find('.row-actions').text(l10n.updating); 561 | 562 | setCurrentAction('update-plugin'); 563 | 564 | actions_updatePlugin({ 565 | slug: $row.data('slug'), 566 | plugin: $row.data('plugin') 567 | }); 568 | }); 569 | 570 | /** 571 | * Handles installing a plugin from clicking `.delete-now` 572 | * link. 573 | * 574 | * @since 1.0.0 575 | * 576 | * @param {Event} event Event interface. 577 | */ 578 | $page.on('click', '.delete-now', function (event) { 579 | event.preventDefault(); 580 | 581 | var $link = $(event.target); 582 | var $row = $link.closest('tr'); 583 | 584 | if ($row.hasClass('updating-message')) { 585 | return; 586 | } 587 | 588 | if (!window.confirm(external_wp_["updates"].l10n.aysDeleteUninstall.replace('%s', $row.find('.plugin-title strong').text()))) { 589 | return; 590 | } 591 | 592 | $row.addClass('updating-message').find('.row-actions').text(l10n.deleting); 593 | 594 | setCurrentAction('delete-plugin'); 595 | 596 | actions_deletePlugin({ 597 | slug: $row.data('slug'), 598 | plugin: $row.data('plugin') 599 | }); 600 | }); 601 | 602 | /** 603 | * Handles the submission of the Bulk Actions. 604 | * 605 | * @since 1.0.0 606 | * 607 | * @param {Event} event Event interface. 608 | */ 609 | $page.on('click', '#do-action-top, #do-action-bottom', function (event) { 610 | event.preventDefault(); 611 | 612 | var $button = $(this); 613 | var action = $button.prev('select').val(); 614 | var $form = $button.closest('form'); 615 | 616 | if ('activate-selected' == action || 'deactivate-selected' == action) { 617 | $form.submit(); 618 | return; 619 | } 620 | 621 | $form.find('input[name="checked[]"]:checked').each(function () { 622 | var $checkbox = $(this); 623 | var $row = $checkbox.closest('tr'); 624 | 625 | queue.push({ 626 | row: $row, 627 | slug: $checkbox.val(), 628 | plugin: $row.data('plugin'), 629 | action: action 630 | }); 631 | }); 632 | 633 | $form.find('input[type="checkbox"]').attr('checked', false); 634 | 635 | $form.find('select').val('-1'); 636 | 637 | queue_processQueue(); 638 | }); 639 | }); 640 | 641 | /***/ }) 642 | /******/ ]); -------------------------------------------------------------------------------- /dist/plugin-manager/assets/js/plugin-manager.min.js: -------------------------------------------------------------------------------- 1 | !function(e){var t={};function n(a){if(t[a])return t[a].exports;var r=t[a]={i:a,l:!1,exports:{}};return e[a].call(r.exports,r,r.exports,n),r.l=!0,r.exports}n.m=e,n.c=t,n.d=function(e,t,a){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:a})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var a=Object.create(null);if(n.r(a),Object.defineProperty(a,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)n.d(a,r,function(t){return e[t]}.bind(null,r));return a},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=4)}([function(e,t){e.exports=jQuery},function(e,t){e.exports=wp},function(e,t){e.exports={usage:"theme",name:"Theme Blvd",namespace:"themeblvd",textDomain:"@@text-domain"}},function(e,t){e.exports=pluginManagerSettings},function(e,t,n){"use strict";n.r(t);var a=n(0),r=n.n(a),s=n(1),l=n(2),u=n(3),i=n.n(u),o=r()(document),g=r.a.extend(i.a,s.updates.l10n),c=Object.assign||function(e){for(var t=1;t Add New` screen. 56 | * 57 | * @since 1.0.0 58 | */ 59 | class Theme_Blvd_Plugin_Manager { 60 | 61 | /** 62 | * Optional class options. 63 | * 64 | * @since 1.0.0 65 | * @see set_args() 66 | * @var array 67 | */ 68 | private $args = array(); 69 | 70 | /** 71 | * Information for package where drop-in is contained. 72 | * 73 | * @since 1.0.0 74 | * @see set_package() 75 | * @var array 76 | */ 77 | private $package; 78 | 79 | /** 80 | * Formatted plugins object. 81 | * 82 | * @since 1.0.0 83 | * @var My_Plugins 84 | */ 85 | private $plugins = array(); 86 | 87 | /** 88 | * Admin screen base ID to match against 89 | * get_current_screen(). 90 | * 91 | * @since 1.0.0 92 | * @var string 93 | */ 94 | private $admin_screen_base; 95 | 96 | /** 97 | * Class constructor. 98 | * 99 | * @since 1.0.0 100 | * 101 | * @param array $plugins Initial, unformatted suggested plugins. 102 | * @param array $args { 103 | * Optional. Overriding class options. 104 | * 105 | * @type string $page_title Title for admin page. 106 | * @type string $views_title Title used for subtle link on Plugins page. 107 | * @type string $extended_title A more descriptive title. 108 | * @type string $tab_title Title for plugin installer tab. 109 | * @type string $menu_title Title for admin sidebar menu. 110 | * @type string $menu_slug Slug used for admin page URL. 111 | * @type string $capability User capability for accessing admin page. 112 | * } 113 | */ 114 | public function __construct( $plugins, $args = array() ) { 115 | 116 | /* 117 | * Temporarily store $args before set_args(). 118 | * 119 | * We need the raw data for set_package(), even 120 | * though it's not all formatted yet. 121 | */ 122 | $this->args = $args; 123 | 124 | /* 125 | * Setup an array of information for the package. 126 | * 127 | * This information also includes figuring the URL and 128 | * path to the drop-in directory, which is needs to happen 129 | * before any files or assets are included. 130 | */ 131 | $this->set_package(); 132 | 133 | // Setup plugins object. 134 | $this->set_plugins( $plugins ); 135 | 136 | // Merge optional arguments with defaults. 137 | $this->set_args( $args ); 138 | 139 | // Setup admin-wide plugin notices. 140 | $this->set_notices(); 141 | 142 | // Add the admin page to manage plugins. 143 | add_action( 'admin_menu', array( $this, 'add_page' ) ); 144 | 145 | // Add the admin page to manage plugins. 146 | add_action( 'admin_enqueue_scripts', array( $this, 'add_assets' ) ); 147 | 148 | // Adds a link to our admin page, from the Installed Plugins screen. 149 | add_filter( 'views_plugins', array( $this, 'add_plugins_view' ) ); 150 | 151 | // Adds a link to our admin page, from Plugin Install screen. 152 | add_filter( 'views_plugin-install', array( $this, 'add_install_view' ) ); 153 | 154 | // Hook Ajax requests. 155 | add_action( 'wp_ajax_themeblvd-row-refresh', array( $this, 'row_refresh' ) ); 156 | 157 | // Hook non-Ajax requests. 158 | add_action( 'current_screen', array( $this, 'request' ) ); 159 | 160 | } 161 | 162 | /** 163 | * Sets an array of information for the current package 164 | * containing the drop-in directory. 165 | * 166 | * By default, the package info is generated, assuming 167 | * this class exists within a parent theme. 168 | * 169 | * For child themes: Pass in `$args['child_theme'] = true` 170 | * when instantiating object. 171 | * 172 | * For plugins: Pass in `$args['plugin_file'] = __FILE__` 173 | * when instantiating object. 174 | * 175 | * @since 1.0.0 176 | */ 177 | private function set_package() { 178 | 179 | $this->package = array( 180 | 'directory' => dirname( __FILE__ ), 181 | ); 182 | 183 | if ( ! empty( $this->args['plugin_file'] ) ) { 184 | 185 | /* 186 | * Setup package info when drop-in is included 187 | * within plugin. 188 | */ 189 | $plugin_file = $this->args['plugin_file']; 190 | 191 | $plugin_slug = explode( '/', plugin_basename( $plugin_file ) ); 192 | 193 | $this->package['slug'] = $plugin_slug[0]; 194 | 195 | $plugin = get_file_data( $plugin_file, array( 196 | 'Name' => 'Plugin Name' 197 | ), 'plugin' ); 198 | 199 | $this->package['name'] = $plugin['Name']; 200 | 201 | /* 202 | * Dynamically set the URI and file path to the 203 | * drop-in directory within the plugin. 204 | * 205 | * This allows the plugin author to place the drop-in 206 | * directory anywhere within their theme. 207 | */ 208 | $plugin_dir = plugin_dir_path( $plugin_file ); 209 | 210 | $dropin_path = str_replace( $plugin_dir, '', $this->package['directory'] ); 211 | 212 | $this->package['url'] = plugin_dir_url( $plugin_file ) . untrailingslashit( $dropin_path ); 213 | 214 | } else { 215 | 216 | /* 217 | * Setup package info when drop-in is included 218 | * within theme. 219 | */ 220 | if ( ! empty( $this->args['child_theme'] ) ) { 221 | $theme = wp_get_theme( get_stylesheet() ); 222 | } else { 223 | $theme = wp_get_theme( get_template() ); 224 | } 225 | 226 | $this->package['slug'] = $theme->get_stylesheet(); 227 | 228 | $this->package['name'] = $theme->get( 'Name' ); 229 | 230 | /* 231 | * Dynamically set the URI and file path to the 232 | * drop-in directory within the theme. 233 | * 234 | * This allows the theme author to place the drop-in 235 | * directory anywhere within their theme. 236 | */ 237 | $theme_dir = $theme->get_stylesheet_directory(); 238 | 239 | $dropin_path = str_replace( $theme_dir, '', $this->package['directory'] ); 240 | 241 | $this->package['url'] = $theme->get_stylesheet_directory_uri() . untrailingslashit( $dropin_path ); 242 | 243 | } 244 | 245 | } 246 | 247 | /** 248 | * Setup plugins object. 249 | * 250 | * @since 1.0.0 251 | * 252 | * @param array $plugins Initial, unformatted plugins passed to plugin manager. 253 | */ 254 | private function set_plugins( $plugins ) { 255 | 256 | /** 257 | * Include Theme_Blvd_Plugins class to store 258 | * plugin data. 259 | */ 260 | require_once( $this->package['directory'] . '/class-theme-blvd-plugins.php' ); 261 | 262 | // Setup plugins. 263 | $this->plugins = new Theme_Blvd_Plugins( $plugins, $this ); 264 | 265 | } 266 | 267 | /** 268 | * Set and format any arguments passed in, to override 269 | * defaults. 270 | * 271 | * @since 1.0.0 272 | * 273 | * @param array $args { 274 | * Any argument overrides passed to the class. 275 | * 276 | * @type string $page_title Title of plugin manager page. 277 | * @type string $views_title Text for link to the plugin manager admin page, which gets inserted at WordPress's main Plugins admin screen. Set to `false` to disable. 278 | * @type string $tab_title Text for link to the plugin manager admin page, which gets inserted at WordPress's Install Plugins admin screen. Set to `false` to disable. 279 | * @type string $extended_title An extended title that gets used in the title tags of links generated from both `$views_title` and `$tab_title`. 280 | * @type string $menu_title Title of plugin manager menu item. 281 | * @type string $parent_slug Parent url slug for plugin manager admin page. 282 | * @type string $menu_slug URL slug for the plugin manager admin page. 283 | * @type string $capability WordPress user capability for plugin manager admin page. 284 | * @type string $nag_action Text of the link, which leads the user to the plugin manager admin page. 285 | * @type string $nag_dimiss Screen reader text to dismiss plugin manager nags. 286 | * @type string $nag_update Message to tell users not all plugins are compatible. 287 | * @type string $nag_install_single Message to install suggested plugins, if only one exists. 288 | * @type string $nag_install_multiple Message to install suggested plugins. 289 | * @type bool $child_theme Whether implementing from a child theme. Required if implementing from a child theme. 290 | * @type string $plugin_file Absolute path to project plugin's main file. Required if implementing from a plugin. 291 | * } 292 | */ 293 | private function set_args( $args = array() ) { 294 | 295 | $this->args = wp_parse_args( $args, array( 296 | 'page_title' => __( 'Suggested Plugins', '@@text-domain' ), 297 | // translators: 1: name of theme 298 | 'views_title' => sprintf( __( 'Suggested by %s', '@@text-domain' ), $this->package['name'] ), 299 | // translators: 1: name of theme 300 | 'tab_title' => sprintf( __( 'Suggested by %s', '@@text-domain' ), $this->package['name'] ), 301 | // translators: 1: name of theme 302 | 'extended_title' => sprintf( __( 'Suggested Plugins by %s', '@@text-domain' ), $this->package['name'] ), 303 | 'menu_title' => '', // Takes on page_title, when left blank. 304 | 'parent_slug' => 'themes.php', 305 | 'menu_slug' => 'suggested-plugins', 306 | 'capability' => 'install_plugins', 307 | 'nag_action' => __( 'Manage suggested plugins', '@@text-domain' ), 308 | 'nag_dismiss' => __( 'Dismiss this notice', '@@text-domain' ), 309 | // translators: 1: name of theme 310 | 'nag_update' => __( 'Not all of your active, suggested plugins are compatible with %s.', '@@text-domain' ), 311 | // translators: 1: name of theme, 2: number of suggested plugins 312 | 'nag_install_single' => __( '%1$s suggests installing %2$s plugin.', '@@text-domain' ), 313 | // translators: 1: name of theme, 2: number of suggested plugins 314 | 'nag_install_multiple' => __( '%1$s suggests installing %2$s plugins.', '@@text-domain' ), 315 | 'child_theme' => false, 316 | 'plugin_file' => '', 317 | )); 318 | 319 | if ( ! $this->args['menu_title'] ) { 320 | $this->args['menu_title'] = $this->args['page_title']; 321 | } 322 | 323 | } 324 | 325 | /** 326 | * Setup admin-wide notices regarding suggested 327 | * plugins. 328 | * 329 | * @since 1.0.0 330 | */ 331 | private function set_notices() { 332 | 333 | /** 334 | * Include Theme_Blvd_Plugin_Notices class to setup 335 | * and display admin-wide notices, if they're needed. 336 | */ 337 | require_once( $this->package['directory'] . '/class-theme-blvd-plugin-notices.php' ); 338 | 339 | // Setup notices. 340 | $args = array( 341 | 'package_name' => $this->package['name'], 342 | 'package_url' => $this->package['url'], 343 | 'admin_url' => $this->get_admin_url(), 344 | 'nag_action' => $this->args['nag_action'], 345 | 'nag_dismiss' => $this->args['nag_dismiss'], 346 | 'nag_update' => $this->args['nag_update'], 347 | 'nag_install_single' => $this->args['nag_install_single'], 348 | 'nag_install_multiple' => $this->args['nag_install_multiple'], 349 | ); 350 | 351 | $notices = new Theme_Blvd_Plugin_Notices( $args, $this, $this->plugins ); 352 | 353 | } 354 | 355 | /** 356 | * Add the suggsted plugin admin page to manage 357 | * plugins. 358 | * 359 | * @since 1.0.0 360 | */ 361 | public function add_page() { 362 | 363 | $this->admin_screen_base = add_theme_page( 364 | $this->args['page_title'], 365 | $this->args['menu_title'], 366 | $this->args['capability'], 367 | $this->args['menu_slug'], 368 | array( $this, 'display_page' ) 369 | ); 370 | 371 | } 372 | 373 | /** 374 | * Add any CSS or JavaScript to plugin manager 375 | * admin page. 376 | * 377 | * @since 1.0.0 378 | */ 379 | public function add_assets() { 380 | 381 | /* 382 | * The plugin-manager .js and .css files should only 383 | * be included on our specific admin page for managing 384 | * plugins. 385 | */ 386 | if ( ! $this->is_admin_screen() ) { 387 | return; 388 | } 389 | 390 | $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; 391 | 392 | wp_enqueue_script( 'updates' ); 393 | 394 | wp_enqueue_script( 395 | 'themeblvd-plugin-manager', 396 | esc_url( $this->package['url'] . "/assets/js/plugin-manager$suffix.js" ), 397 | array( 'jquery', 'updates' ) 398 | ); 399 | 400 | wp_localize_script( 401 | 'themeblvd-plugin-manager', 402 | 'pluginManagerSettings', 403 | array( 404 | 'thirdParty' => __( 'Only plugins from wordpress.org can be installed directly here.', '@@text-domain' ), 405 | 'notInstalled' => __( 'Plugin update skipped because it is not installed.', '@@text-domain' ), 406 | ) 407 | ); 408 | 409 | wp_enqueue_style( 410 | 'themeblvd-plugin-manager', 411 | esc_url( $this->package['url'] . "/assets/css/plugin-manager$suffix.css" ) 412 | ); 413 | 414 | } 415 | 416 | /** 417 | * Display the suggsted plugin admin page to 418 | * manage plugins. 419 | * 420 | * @since 1.0.0 421 | */ 422 | public function display_page() { 423 | 424 | $plugins = $this->plugins->get(); 425 | 426 | settings_errors( 'plugin-manager' ); 427 | 428 | ?> 429 |
430 | 431 |

args['page_title'] ); ?>

432 | 433 | 434 | 435 |
436 | 437 | display_table_nav( 'top' ); ?> 438 | 439 | 440 | 441 | display_table_header( 'thead' ); ?> 442 | 443 | 444 | 445 | 446 | 447 | display_table_row( $plugin ); ?> 448 | 449 | 450 | 451 | 452 | 453 | display_table_header( 'tfoot' ); ?> 454 | 455 |
456 | 457 | display_table_nav( 'bottom' ); ?> 458 | 459 |
460 | 461 | 462 | 463 |

464 | 465 | 466 | 467 |
468 | __( 'Install', '@@text-domain' ), 483 | 'activate-selected' => __( 'Activate', '@@text-domain' ), 484 | 'deactivate-selected' => __( 'Deactivate', '@@text-domain' ), 485 | 'update-selected' => __( 'Update', '@@text-domain' ), 486 | 'delete-selected' => __( 'Delete', '@@text-domain' ), 487 | ); 488 | 489 | if ( 'top' === $which ) { 490 | wp_nonce_field( 'bulk-plugins' ); 491 | } 492 | 493 | ?> 494 |
495 |
496 | 497 | 500 | 501 | 512 | 513 | "do-action-$which", 517 | ) 518 | ); 519 | ?> 520 | 521 |
522 |
523 | 539 | <> 540 | 541 | 542 | 543 | 544 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | > 569 | 600 | 601 | 602 | 603 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 |
619 | display_actions( $plugin ); ?> 620 |
621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | display_status( $plugin ); ?> 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 |
650 |

651 |
652 | 653 | 654 | 655 | 656 | 657 | 658 | $plugin['url'], 687 | 'text' => __( 'Details', '@@text-domain' ), 688 | // translators: 1: name of plugin 689 | 'label' => sprintf( __( 'More Information about %s', '@@text-domain' ), $plugin['name'] ), 690 | 'target' => '_blank', 691 | 'nonce' => null, 692 | ); 693 | 694 | /* 695 | if ( $is_wp ) { 696 | $actions['details']['target'] = '_self'; 697 | $actions['details']['class'] = 'thickbox open-plugin-details-modal'; 698 | } 699 | */ 700 | } 701 | 702 | /* 703 | * Add "Install" or "Get Plugin" link. 704 | * 705 | * "Install" - If the plugin isn't installed and it's 706 | * located in the wordpress.org repo, give the user 707 | * an install link. 708 | * 709 | * "Get Plugin" - For a plugin not installed from 710 | * another source, give them an external URL to get 711 | * the plugin. 712 | */ 713 | if ( 'not-installed' === $plugin['status'] ) { 714 | 715 | if ( $is_wp ) { 716 | 717 | /* 718 | * This URL is a fallback for when JavaScript is broken 719 | * or disabled. It will lead to /wp-admin/update.php and 720 | * trigger default action of install a plugin. 721 | */ 722 | $url = add_query_arg( 723 | array( 724 | 'action' => 'install-plugin', 725 | 'plugin' => urlencode( $plugin['slug'] ), 726 | '_wpnonce' => wp_create_nonce( 'install-plugin_' . $plugin['slug'] ), // Formatted for WP's update.php. 727 | ), 728 | self_admin_url( 'update.php' ) 729 | ); 730 | 731 | $actions['install'] = array( 732 | 'url' => $url, 733 | 'text' => __( 'Install', '@@text-domain' ), 734 | // translators: 1: name of plugin 735 | 'label' => sprintf( __( 'Install %s', '@@text-domain' ), $plugin['name'] ), 736 | 'target' => '_self', 737 | 'nonce' => null, // No nonce needed, using wp.updates.ajaxNonce. 738 | ); 739 | 740 | } else { 741 | 742 | $actions['install'] = array( 743 | 'url' => $plugin['url'], 744 | 'text' => __( 'Get Plugin', '@@text-domain' ), 745 | // translators: 1: name of plugin 746 | 'label' => sprintf( __( 'Install %s', '@@text-domain' ), $plugin['name'] ), 747 | 'target' => '_blank', 748 | 'nonce' => null, 749 | ); 750 | 751 | } 752 | } 753 | 754 | // Add "Activate" or "Deactivate" link. 755 | if ( 'inactive' === $plugin['status'] ) { 756 | 757 | $url = add_query_arg( 758 | array( 759 | 'action' => 'activate', 760 | 'plugin' => urlencode( $plugin['slug'] ), 761 | '_wpnonce' => wp_create_nonce( 'plugin-request_' . $plugin['file'] ), 762 | ), 763 | $this->get_admin_url() 764 | ); 765 | 766 | $actions['activate'] = array( 767 | 'url' => $url, 768 | 'text' => __( 'Activate', '@@text-domain' ), 769 | // translators: 1: name of plugin 770 | 'label' => sprintf( __( 'Activate %s', '@@text-domain' ), $plugin['name'] ), 771 | 'target' => '_self', 772 | ); 773 | 774 | } elseif ( 'active' === $plugin['status'] || 'incompatible' === $plugin['status'] ) { 775 | 776 | $url = add_query_arg( 777 | array( 778 | 'action' => 'deactivate', 779 | 'plugin' => urlencode( $plugin['slug'] ), 780 | '_wpnonce' => wp_create_nonce( 'plugin-request_' . $plugin['file'] ), 781 | ), 782 | $this->get_admin_url() 783 | ); 784 | 785 | $actions['deactivate'] = array( 786 | 'url' => $url, 787 | 'text' => __( 'Deactivate', '@@text-domain' ), 788 | // translators: 1: name of plugin 789 | 'label' => sprintf( __( 'Deactivate %s', '@@text-domain' ), $plugin['name'] ), 790 | 'target' => '_self', 791 | 'nonce' => wp_create_nonce( 'plugin-activation_' . $plugin['slug'] ), 792 | ); 793 | 794 | } 795 | 796 | // Add "Delete" link. 797 | if ( 'inactive' === $plugin['status'] ) { 798 | 799 | /* 800 | * This URL is a fallback for when JavaScript is broken 801 | * or disabled. It will lead to /wp-admin/plugins.php and 802 | * trigger default action of deleting a plugin. 803 | */ 804 | $url = add_query_arg( 805 | array( 806 | 'action' => 'delete-selected', 807 | 'verify-delete' => '1', 808 | 'checked[]' => urlencode( $plugin['file'] ), 809 | '_wpnonce' => wp_create_nonce( 'bulk-plugins' ), // Formatted for plugins.php 810 | ), 811 | self_admin_url( 'plugins.php' ) 812 | ); 813 | 814 | $actions['delete'] = array( 815 | 'url' => $url, 816 | 'text' => __( 'Delete', '@@text-domain' ), 817 | // translators: 1: name of plugin 818 | 'label' => sprintf( __( 'Delete %s', '@@text-domain' ), $plugin['name'] ), 819 | 'target' => '_self', 820 | 'nonce' => wp_create_nonce( 'delete-plugin_' . $plugin['slug'] ), 821 | ); 822 | 823 | } 824 | 825 | // Add "Update to version {version}" link or "Plugin is up-to-date" message. 826 | if ( $plugin['update'] ) { 827 | 828 | /* 829 | * This URL is a fallback for when JavaScript is broken 830 | * or disabled. It will lead to /wp-admin/update.php and 831 | * trigger default action of updating a plugin. 832 | */ 833 | $url = add_query_arg( 834 | array( 835 | 'action' => 'upgrade-plugin', 836 | 'plugin' => urlencode( $plugin['file'] ), 837 | '_wpnonce' => wp_create_nonce( 'upgrade-plugin_' . $plugin['file'] ), // Formatted for update.php 838 | ), 839 | self_admin_url( 'update.php' ) 840 | ); 841 | 842 | $actions['update'] = array( 843 | 'url' => $url, 844 | 'text' => sprintf( 845 | // translators: 1: new version of plugin 846 | __( 'Update to %s', '@@text-domain' ), 847 | $plugin['new_version'] 848 | ), 849 | 'label' => sprintf( 850 | // translators: 1: name of plugin, 2: new version of plugin 851 | __( 'Update %1$s to version %2$s', '@@text-domain' ), 852 | $plugin['name'], 853 | $plugin['new_version'] 854 | ), 855 | 'target' => '_self', 856 | 'nonce' => null, // No nonce needed, using wp.updates.ajaxNonce. 857 | ); 858 | 859 | } elseif ( 'not-installed' !== $plugin['status'] ) { 860 | 861 | $actions['update'] = array( 862 | 'text' => __( 'Plugin is up-to-date', '@@text-domain' ), 863 | ); 864 | 865 | } 866 | 867 | // Build $output from $actions array data. 868 | $output = array(); 869 | 870 | foreach ( $actions as $key => $action ) { 871 | 872 | if ( ! empty( $action['url'] ) ) { 873 | 874 | $class = ''; 875 | 876 | if ( false !== strpos( $action['url'], get_site_url() ) ) { 877 | $class = $key . '-now'; 878 | } 879 | 880 | if ( ! empty( $action['class'] ) ) { 881 | $class .= ' ' . $action['class']; 882 | } 883 | 884 | $item = sprintf( 885 | '%s', 886 | $key, 887 | esc_url( $action['url'] ), 888 | esc_attr( $class ), 889 | esc_attr( $action['label'] ), 890 | esc_attr( $action['target'] ), 891 | esc_html( $action['text'] ) 892 | ); 893 | 894 | if ( ! empty( $action['nonce'] ) ) { 895 | 896 | $item = str_replace( 897 | 'href', 898 | sprintf( 'data-ajax-nonce="%s" href', $action['nonce'] ), 899 | $item 900 | ); 901 | 902 | } 903 | } else { 904 | 905 | $item = sprintf( 906 | '%s', 907 | $key, 908 | esc_html( $action['text'] ) 909 | ); 910 | 911 | } 912 | 913 | $output[] = $item; 914 | 915 | } 916 | 917 | echo implode( ' | ', $output ); 918 | 919 | } 920 | 921 | /** 922 | * Display the status of a plugin. 923 | * 924 | * For more on how status is determined see docs 925 | * for set_plugins() method. 926 | * 927 | * @since 1.0.0 928 | * 929 | * @param array $plugin Plugin data. 930 | */ 931 | private function display_status( $plugin ) { 932 | 933 | switch ( $plugin['status'] ) { 934 | case 'active': 935 | esc_html_e( 'Active', '@@text-domain' ); 936 | break; 937 | 938 | case 'incompatible': 939 | esc_html_e( 'Incompatible', '@@text-domain' ); 940 | break; 941 | 942 | case 'inactive': 943 | esc_html_e( 'Installed', '@@text-domain' ); 944 | break; 945 | 946 | default: 947 | esc_html_e( 'Not Installed', '@@text-domain' ); 948 | } 949 | 950 | } 951 | 952 | /** 953 | * Adds a link to Plugins screen that links 954 | * to our plugin manager admin page. 955 | * 956 | * @since 1.0.0 957 | */ 958 | public function add_plugins_view( $views ) { 959 | 960 | if ( ! $this->args['views_title'] ) { 961 | return $views; 962 | } 963 | 964 | $plugins = $this->plugins->get(); 965 | 966 | if ( $plugins ) { 967 | 968 | $views['suggested'] = sprintf( 969 | "%s (%d)", 970 | esc_url( $this->get_admin_url() ), 971 | esc_html( $this->args['extended_title'] ), 972 | esc_html( $this->args['views_title'] ), 973 | count( $plugins ) 974 | ); 975 | 976 | } 977 | 978 | return $views; 979 | 980 | } 981 | 982 | /** 983 | * Add tab to Plugin Installer screen that links 984 | * to our plugin manager admin page. 985 | * 986 | * @since 1.0.0 987 | */ 988 | public function add_install_view( $tabs ) { 989 | 990 | if ( ! $this->args['tab_title'] ) { 991 | return $tabs; 992 | } 993 | 994 | if ( $this->plugins->get() ) { 995 | 996 | $tabs['suggested-by-theme'] = sprintf( 997 | "%s", 998 | esc_url( $this->get_admin_url() ), 999 | esc_html( $this->args['extended_title'] ), 1000 | esc_html( $this->args['tab_title'] ) 1001 | ); 1002 | 1003 | } 1004 | 1005 | return $tabs; 1006 | 1007 | } 1008 | 1009 | /** 1010 | * Get the URL to the admin page to manage plugins. 1011 | * 1012 | * @since 1.0.0 1013 | * 1014 | * @return string URL to plugin-manager admin page. 1015 | */ 1016 | public function get_admin_url() { 1017 | 1018 | return add_query_arg( 1019 | 'page', 1020 | $this->args['menu_slug'], 1021 | admin_url( $this->args['parent_slug'] ) 1022 | ); 1023 | 1024 | } 1025 | 1026 | /** 1027 | * Get source to display for a plugin. 1028 | * 1029 | * @since 1.0.0 1030 | * 1031 | * @param array $plugin Plugin data. 1032 | * @return string Plugin source, `wordpress.org` or `third-party`. 1033 | */ 1034 | public function get_plugin_source( $plugin ) { 1035 | 1036 | if ( false === strpos( $plugin['url'], 'wordpress.org' ) ) { 1037 | return 'third-party'; 1038 | } 1039 | 1040 | return 'wordpress.org'; 1041 | 1042 | } 1043 | 1044 | /** 1045 | * Check whether we're currently on the plugin 1046 | * manager admin page or not. 1047 | * 1048 | * @since 1.0.0 1049 | * 1050 | * @return bool If plugin-manager admin page. 1051 | */ 1052 | public function is_admin_screen() { 1053 | 1054 | $screen = get_current_screen(); 1055 | 1056 | if ( $screen && $this->admin_screen_base === $screen->base ) { 1057 | return true; 1058 | } 1059 | 1060 | return false; 1061 | 1062 | } 1063 | 1064 | /** 1065 | * Handle Ajax request when a plugin's status has 1066 | * changed and its table row needs to be refreshed. 1067 | * 1068 | * @since 1.0.0 1069 | */ 1070 | public function row_refresh() { 1071 | 1072 | check_ajax_referer( 'updates' ); 1073 | 1074 | if ( ! $this->plugins->is_set() ) { 1075 | $this->plugins->set(); 1076 | } 1077 | 1078 | $plugin = $this->plugins->get( $_POST['slug'] ); 1079 | 1080 | if ( $plugin ) { 1081 | 1082 | if ( ! empty( $_POST['error'] ) ) { 1083 | 1084 | $plugin['notice'] = array( 1085 | 'class' => 'notice-' . $_POST['error_level'], 1086 | 'message' => $_POST['error'], 1087 | ); 1088 | 1089 | $plugin['row-class'] = array( 'row-has-notice' ); 1090 | 1091 | } else { 1092 | 1093 | $plugin['row-class'] = array( 'ajax-success' ); 1094 | 1095 | if ( ! empty( $_POST['prev_action'] ) ) { 1096 | 1097 | $action_slug = str_replace( '-plugin', '', $_POST['prev_action'] ); 1098 | 1099 | $plugin['row-class'][] = $action_slug . '-success'; 1100 | 1101 | } 1102 | } 1103 | 1104 | $this->display_table_row( $plugin ); 1105 | 1106 | } 1107 | 1108 | wp_die(); 1109 | 1110 | } 1111 | 1112 | /** 1113 | * Handles any non-Ajax request. 1114 | * 1115 | * We use this for activating and deactivating plugins 1116 | * because these actions require that the WordPress 1117 | * admin is refreshed. So handling them through Ajax 1118 | * is a bit unnecessary. 1119 | * 1120 | * This method handles both the activation and 1121 | * deactivation processes, along with error message 1122 | * handling and success message on redirect. 1123 | * 1124 | * Also, single plugin and bulk processing is supported. 1125 | * 1126 | * @since 1.0.0 1127 | */ 1128 | public function request() { 1129 | 1130 | global $_REQUEST; 1131 | 1132 | // Do nothing if this isn't our admin screen. 1133 | if ( ! $this->is_admin_screen() ) { 1134 | return; 1135 | } 1136 | 1137 | // Check for bulk actions. 1138 | $do_bulk = false; 1139 | 1140 | if ( isset( $_REQUEST['action-top'] ) || isset( $_REQUEST['action-bottom'] ) ) { 1141 | 1142 | $do_bulk = true; 1143 | 1144 | if ( ! empty( $_REQUEST['action-top'] ) && -1 != $_REQUEST['action-top'] ) { 1145 | 1146 | $_REQUEST['action'] = $_REQUEST['action-top']; 1147 | 1148 | } elseif ( ! empty( $_REQUEST['action-bottom'] ) && -1 != $_REQUEST['action-bottom'] ) { 1149 | 1150 | $_REQUEST['action'] = $_REQUEST['action-bottom']; 1151 | 1152 | } 1153 | } 1154 | 1155 | if ( empty( $_REQUEST['action'] ) ) { 1156 | return; 1157 | } 1158 | 1159 | $_SERVER['REQUEST_URI'] = remove_query_arg( 1160 | array( 1161 | 'action', 1162 | 'success', 1163 | '_error_nonce', 1164 | ), 1165 | $_SERVER['REQUEST_URI'] 1166 | ); 1167 | 1168 | /* 1169 | * If this was a redirect from a successful activate 1170 | * or deactivate action, we'll just add a success 1171 | * message and then return. 1172 | */ 1173 | if ( isset( $_REQUEST['success'] ) ) { 1174 | 1175 | $message = ''; 1176 | 1177 | if ( 'activate' === $_REQUEST['action'] ) { 1178 | 1179 | $message = __( 'Plugin activated.', '@@text-domain' ); 1180 | 1181 | } elseif ( 'activate-selected' === $_REQUEST['action'] ) { 1182 | 1183 | $num = $_REQUEST['success']; 1184 | 1185 | $message = sprintf( 1186 | // translators: 1: number of plugins activated without error 1187 | _n( 1188 | '%s plugin activated successfully.', 1189 | '%s plugins activated successfully.', 1190 | $num, 1191 | '@@text-domain' 1192 | ), 1193 | $num 1194 | ); 1195 | 1196 | } elseif ( 'deactivate' === $_REQUEST['action'] ) { 1197 | 1198 | $message = __( 'Plugin deactivated.', '@@text-domain' ); 1199 | 1200 | } elseif ( 'deactivate-selected' === $_REQUEST['action'] ) { 1201 | 1202 | $message = __( 'Plugins deactivated.', '@@text-domain' ); 1203 | 1204 | } 1205 | 1206 | if ( $message ) { 1207 | 1208 | add_settings_error( 1209 | 'plugin-manager', 1210 | 'plugin-manager-error', 1211 | $message, 1212 | 'updated' 1213 | ); 1214 | 1215 | } 1216 | 1217 | return; 1218 | 1219 | } 1220 | 1221 | // Get all plugin data. 1222 | $plugins_data = $this->plugins->get(); 1223 | 1224 | /* 1225 | * Perform error checking, to see if we have everything 1226 | * to get started activating or deactivating plugins. 1227 | */ 1228 | $error = ''; 1229 | 1230 | if ( $do_bulk && empty( $_REQUEST['checked'] ) ) { 1231 | 1232 | $error = __( 'No plugins were selected.', '@@text-domain' ); 1233 | 1234 | } elseif ( ! $do_bulk && empty( $_REQUEST['plugin'] ) ) { 1235 | 1236 | $error = __( 'No plugin slug was given.', '@@text-domain' ); 1237 | 1238 | } elseif ( ! current_user_can( 'update_plugins' ) ) { 1239 | 1240 | $error = __( 'Sorry, you are not allowed to update plugins for this site.', '@@text-domain' ); 1241 | 1242 | } elseif ( ! $do_bulk && empty( $plugins_data[ $_REQUEST['plugin'] ] ) ) { 1243 | 1244 | $error = sprintf( 1245 | // translators: 1: slug of plugin being activated 1246 | __( 'The plugin %s doesn\'t exist within the plugin manager\'s registered plugins.', '@@text-domain' ), 1247 | $_REQUEST['plugin'] 1248 | ); 1249 | 1250 | } 1251 | 1252 | /* 1253 | * If no errors were found above, we can begin activating 1254 | * or deactivating plugins. 1255 | */ 1256 | if ( ! $error ) { 1257 | 1258 | $success = 0; // Count of how many successful plugins activated on bulk. 1259 | 1260 | $redirect = add_query_arg( 1261 | array( 1262 | 'action' => $_REQUEST['action'], 1263 | 'success' => 1, 1264 | ), 1265 | $this->get_admin_url() 1266 | ); 1267 | 1268 | if ( $do_bulk ) { 1269 | 1270 | $plugins = array(); 1271 | 1272 | foreach ( $_REQUEST['checked'] as $slug ) { 1273 | 1274 | if ( ! empty( $plugins_data[ $slug ] ) ) { 1275 | 1276 | $plugins[ $slug ] = $plugins_data[ $slug ]; 1277 | 1278 | } 1279 | } 1280 | 1281 | if ( ! $plugins ) { 1282 | $error = __( 'No valid plugins given for bulk action.', '@@text-domain' ); 1283 | } 1284 | } else { 1285 | 1286 | $plugin = $plugins_data[ $_REQUEST['plugin'] ]; 1287 | 1288 | } 1289 | 1290 | // Perform action. 1291 | if ( 'activate' === $_REQUEST['action'] ) { 1292 | 1293 | $result = activate_plugin( $plugin['file'], $redirect ); 1294 | 1295 | } elseif ( 'activate-selected' === $_REQUEST['action'] ) { 1296 | 1297 | foreach ( $plugins as $plugin ) { 1298 | 1299 | $result = activate_plugin( $plugin['file'] ); 1300 | 1301 | if ( ! is_wp_error( $result ) ) { 1302 | $success++; 1303 | } 1304 | } 1305 | 1306 | if ( $success ) { // At least one plugin needed to be successful. 1307 | 1308 | $redirect = add_query_arg( 'success', $success, $redirect ); 1309 | 1310 | wp_redirect( $redirect ); 1311 | 1312 | } else { 1313 | 1314 | $error = __( 'None of the selected plugins could be activated. Make sure they are installed.', '@@text-domain' ); 1315 | 1316 | } 1317 | } elseif ( 'deactivate' === $_REQUEST['action'] || 'deactivate-selected' === $_REQUEST['action'] ) { 1318 | 1319 | $deactivate = array(); 1320 | 1321 | if ( $do_bulk ) { 1322 | 1323 | foreach ( $plugins as $plugin ) { 1324 | $deactivate[] = trailingslashit( WP_PLUGIN_DIR ) . $plugin['file']; 1325 | } 1326 | } else { 1327 | 1328 | $deactivate[] = trailingslashit( WP_PLUGIN_DIR ) . $plugin['file']; 1329 | 1330 | } 1331 | 1332 | $result = deactivate_plugins( $deactivate ); 1333 | 1334 | if ( ! is_wp_error( $result ) ) { 1335 | wp_redirect( $redirect ); 1336 | } 1337 | } 1338 | 1339 | if ( ! $error && is_wp_error( $result ) ) { 1340 | $error = $result->get_error_message(); 1341 | } 1342 | } 1343 | 1344 | /* 1345 | * If an error was found at any point, we can queue it to 1346 | * display. 1347 | * 1348 | * Otherwise, if an activation or deactivation were successful, 1349 | * the user would have already been redirected by this point. 1350 | */ 1351 | if ( $error ) { 1352 | 1353 | add_settings_error( 1354 | 'plugin-manager', 1355 | 'plugin-manager-error', 1356 | $error, 1357 | 'error' 1358 | ); 1359 | 1360 | } 1361 | 1362 | } 1363 | } 1364 | } 1365 | -------------------------------------------------------------------------------- /dist/plugin-manager/class-theme-blvd-plugin-notices.php: -------------------------------------------------------------------------------- 1 | args = wp_parse_args( $args, array( 81 | 'package_name' => '', 82 | 'package_url' => '', 83 | 'admin_url' => '', 84 | 'nag_action' => '', 85 | 'nag_dismiss' => '', 86 | 'nag_update' => '', 87 | 'nag_install_single' => '', 88 | 'nag_install_multiple' => '', 89 | )); 90 | 91 | $this->manager = $manager; 92 | 93 | $this->plugins = $plugins; 94 | 95 | // Determine admin-wide notices. 96 | add_action( 'admin_enqueue_scripts', array( $this, 'set_notices' ) ); 97 | 98 | // Print necessary scripts for admin-wide notices. 99 | add_action( 'admin_enqueue_scripts', array( $this, 'add_assets' ) ); 100 | 101 | // Display admin-wide notices. 102 | add_action( 'admin_notices', array( $this, 'add_notices' ) ); 103 | 104 | // Handle notice dismissals through Ajax. 105 | add_action( 'wp_ajax_themeblvd-dismiss-notice', array( $this, 'dismiss' ) ); 106 | 107 | } 108 | 109 | /** 110 | * Sets admin plugin notices, which are seen 111 | * throughout the entire admin. 112 | * 113 | * @since 1.0.0 114 | */ 115 | public function set_notices() { 116 | 117 | // DEBUG: 118 | // delete_user_meta( get_current_user_id(), 'themeblvd-dismiss-plugin-notice_update' ); 119 | // delete_user_meta( get_current_user_id(), 'themeblvd-dismiss-plugin-notice_install' ); 120 | 121 | $this->notices = array(); 122 | 123 | /* 124 | * Because our notices are intended to lead the user to 125 | * our admin screen, the notices should show everywhere 126 | * except our admin screen. 127 | */ 128 | if ( $this->manager->is_admin_screen() ) { 129 | return; 130 | } 131 | 132 | $plugins = $this->plugins->get(); 133 | 134 | /* 135 | * If necessary, add notice to install plugins. 136 | * 137 | * If the message hasn't been dismissed, this will tell the 138 | * user how many suggested plugins have the `not-installed` 139 | * status. 140 | */ 141 | $to_install = array(); 142 | 143 | foreach ( $plugins as $plugin ) { 144 | if ( 'not-installed' === $plugin['status'] ) { 145 | $to_install[] = $plugin['slug']; 146 | } 147 | } 148 | 149 | if ( $to_install ) { 150 | 151 | /* 152 | * For the install notice, we want to use all suggested 153 | * plugins, and not just the count of plugins still to 154 | * be installed. 155 | * 156 | * This is so if the user dimisses the notice, they won't 157 | * see it again by installing and uninstalling plugins. 158 | * However, they will see it if the theme author adds 159 | * more suggested plugins. 160 | */ 161 | $check = implode( ',', array_keys( $plugins ) ); // All suggested plugins, not just those in $to_install. 162 | 163 | if ( get_user_meta( get_current_user_id(), 'themeblvd-dismiss-plugin-notice_install', true ) !== $check ) { 164 | 165 | $count = count( $to_install ); 166 | 167 | if ( 0 === $count || $count >= 2 ) { 168 | $plural = 'multiple'; 169 | } else { 170 | $plural = 'single'; 171 | } 172 | 173 | $this->notices['install']['message'] = sprintf( 174 | $this->args[ 'nag_install_' . $plural ], 175 | $this->args['package_name'], 176 | $count 177 | ); 178 | 179 | $this->notices['install']['value'] = $check; 180 | 181 | } 182 | } 183 | 184 | /* 185 | * If necessary, add notice to update plugins. 186 | * 187 | * If the message hasn't been dismissed, this will tell the 188 | * user how many plugins have status `incompatible`. 189 | * 190 | * For a plugin to be marked incompatible, it must be active 191 | * and it's suggested version must be higher than its 192 | * installed version. 193 | */ 194 | $to_update = array(); 195 | 196 | foreach ( $plugins as $plugin ) { 197 | if ( 'incompatible' === $plugin['status'] ) { 198 | $to_update[] = $plugin['slug']; 199 | } 200 | } 201 | 202 | if ( $to_update ) { 203 | 204 | /* 205 | * For the update notice, we want the notice to appear 206 | * any time the string of incompatible plugins changes. 207 | */ 208 | $check = implode( ',', $to_update ); 209 | 210 | if ( get_user_meta( get_current_user_id(), 'themeblvd-dismiss-plugin-notice_update', true ) !== $check ) { 211 | 212 | $this->notices['update']['message'] = sprintf( 213 | $this->args['nag_update'], 214 | $this->args['package_name'] 215 | ); 216 | 217 | $this->notices['update']['value'] = $check; 218 | 219 | } 220 | } 221 | 222 | } 223 | 224 | /** 225 | * Add any assets needed for plugin notices. 226 | * 227 | * @since 1.0.0 228 | */ 229 | public function add_assets() { 230 | 231 | if ( ! $this->notices ) { 232 | return; 233 | } 234 | 235 | $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; 236 | 237 | wp_enqueue_script( 238 | 'themeblvd-plugin-notices', 239 | esc_url( $this->args['package_url'] . "/assets/js/plugin-notices$suffix.js" ), 240 | array( 'jquery' ) 241 | ); 242 | 243 | } 244 | 245 | /** 246 | * Handles admin plugin notices, which are seen 247 | * throughout the entire admin. 248 | * 249 | * @since 1.0.0 250 | */ 251 | public function add_notices() { 252 | 253 | if ( $this->notices ) { 254 | 255 | foreach ( $this->notices as $key => $data ) { 256 | 257 | $this->display( $key, $data['value'], $data['message'] ); 258 | 259 | } 260 | } 261 | 262 | } 263 | 264 | /** 265 | * Display custom admin notice. 266 | * 267 | * @since 1.0.0 268 | */ 269 | public function display( $key, $value, $message ) { 270 | 271 | $class = 'plugin-manager-notice notice is-dismissible'; 272 | 273 | if ( 'update' === $key ) { 274 | $class .= ' notice-warning'; 275 | } else { 276 | $class .= ' notice-info'; 277 | } 278 | 279 | $nonce = wp_create_nonce( 'plugin-manager-dismiss-notice_' . $key ); 280 | 281 | $action_link = sprintf( 282 | '%2$s', 283 | esc_url( $this->args['admin_url'] ), 284 | esc_html( $this->args['nag_action'] ) 285 | ); 286 | 287 | $dismiss_link = sprintf( 288 | '%2$s', 289 | esc_url( $this->args['admin_url'] ), 290 | esc_html( $this->args['nag_dismiss'] ) 291 | ); 292 | 293 | ?> 294 |
295 | 296 |

297 | 298 |

299 | 300 |
301 | pre_plugins = $pre_plugins; 58 | 59 | $this->manager = $manager; 60 | 61 | /* 62 | * Setup plugins. The formatting for this must get 63 | * hooked later in the WordPress loading process to 64 | * make use of /wp-admin/includes/plugin.php. 65 | */ 66 | add_action( 'current_screen', array( $this, 'set' ) ); 67 | 68 | } 69 | 70 | /** 71 | * Consistently format plugins and add some 72 | * helpful info. 73 | * 74 | * @since 1.0.0 75 | */ 76 | public function set() { 77 | 78 | /* 79 | * If we're loading our admin screen, we can refresh 80 | * the WordPress plugin update cache to show the 81 | * latest results. 82 | */ 83 | if ( $this->manager->is_admin_screen() ) { 84 | wp_update_plugins(); 85 | } 86 | 87 | /* 88 | * Get all currently installed plugins, that exist 89 | * from suggested plugin list, including which have 90 | * updates ready to install. 91 | */ 92 | $installed = $this->get_installed(); 93 | 94 | /* 95 | * Loop through passed in plugins, looking for matches 96 | * with installed plugins, and store results to $plugins. 97 | */ 98 | foreach ( $this->pre_plugins as $plugin ) { 99 | 100 | $info = array(); 101 | 102 | if ( array_key_exists( $plugin['slug'], $installed ) ) { 103 | $info = $installed[ $plugin['slug'] ]; 104 | } 105 | 106 | $this->add( $plugin, $info ); 107 | 108 | } 109 | 110 | // Alphabatize plugins by slug. 111 | ksort( $this->plugins ); 112 | 113 | } 114 | 115 | /** 116 | * Check if plugins have been setup and formatted. 117 | * 118 | * @since 1.0.0 119 | * 120 | * @return bool If $plugins has been properly setup. 121 | */ 122 | public function is_set() { 123 | 124 | if ( $this->plugins ) { 125 | return true; 126 | } 127 | 128 | return false; 129 | 130 | } 131 | 132 | /** 133 | * Format and store plugin to $this->plugins. 134 | * 135 | * The "status" of a plugin can be one of the following: 136 | * 137 | * 1. `not-installed` It's simply not installed. 138 | * 2. `inactive` Installed, but not activated yet. 139 | * 3. `incompatible` Activated, but installed version is less than suggested version. 140 | * 4. `active` Installed, activated and compatible. 141 | * 142 | * Note: The `incompatible` status will only be applied 143 | * if the current version is less than the suggsted version; 144 | * it will NOT be applied simply because WordPress says 145 | * the plugin has an update available. 146 | * 147 | * The final stored plugin data will be an array formatted, 148 | * as follows. 149 | * 150 | * $plugin { 151 | * @type string $name Name of plugin, like `My Plugin`. 152 | * @type string $slug Slug of plugin, like `my-plugin`. 153 | * @type string $url URL to plugin website, ONLY if not on wordpress.org. 154 | * @type string $version Suggested plugin version, like `2.0+`. 155 | * @type string $current_version Current installed version of plugin. 156 | * @type string $new_version Latest available version of plugin. 157 | * @type string $file Plugin file, like `my-plugin/my-plugin.php`. 158 | * @type string $status Plugin status key (see above). 159 | * @type bool $update Whether WordPress says the plugin has an update available. 160 | * @type array $notice { 161 | * Optional. Notice for plugin, if needed. 162 | * 163 | * @type string $message Message for notice. 164 | * @type string $class CSS class(es) for notice. 165 | * } 166 | * } 167 | * 168 | * @since 1.0.0 169 | * 170 | * @param array $plugin { 171 | * Plugin info from initial object creation. 172 | * 173 | * @type string $name Name of plugin, like `My Plugin`. 174 | * @type string $slug Slug of plugin, like `my-plugin`. 175 | * @type string $url URL to plugin website, ONLY if not on wordpress.org. 176 | * @type string $version Suggested plugin version, like `2.0+`. 177 | * } 178 | * @param array $info { 179 | * Optional. Plugin info from WP, if installed. 180 | * 181 | * @type string $file Location of plugin file. 182 | * @type string $current_version Current installed version. 183 | * @type string $new_version Newest version available. 184 | * @type bool $is_active Whether plugin is active. 185 | * } 186 | */ 187 | public function add( $plugin, $info = null ) { 188 | 189 | // Merge initial plugin data with data abstract. 190 | $plugin = array_merge( array( 191 | 'name' => null, 192 | 'slug' => null, 193 | 'url' => null, 194 | 'version' => null, 195 | 'current_version' => null, 196 | 'new_version' => null, 197 | 'file' => null, 198 | 'status' => 'not-installed', 199 | 'update' => false, 200 | 'notice' => '', 201 | ), $plugin ); 202 | 203 | /* 204 | * If installed data is null, it means it hasn't been 205 | * attempted to be retrieved. If it has been retrieved 206 | * but was empty (i.e plugin isn't' installed), then 207 | * an empty array should be passed for $info. 208 | */ 209 | if ( null === $info ) { 210 | 211 | $installed = $this->get_installed(); 212 | 213 | if ( array_key_exists( $plugin['slug'], $installed ) ) { 214 | $info = $installed[ $plugin['slug'] ]; 215 | } 216 | } 217 | 218 | /* 219 | * At this point, if $info is empty the plugin status 220 | * shall remain as 'not-installed' and the following 221 | * setup will be skipped. 222 | */ 223 | if ( $info ) { 224 | 225 | // Add plugin file location. 226 | if ( ! empty( $info['file'] ) ) { 227 | $plugin['file'] = $info['file']; 228 | } 229 | 230 | // Add currently installed version. 231 | if ( ! empty( $info['current_version'] ) ) { 232 | $plugin['current_version'] = $info['current_version']; 233 | } 234 | 235 | // Add latest available version to check against current. 236 | if ( ! empty( $info['new_version'] ) ) { 237 | $plugin['new_version'] = $info['new_version']; 238 | } 239 | 240 | // Set plugin status. 241 | if ( ! empty( $info['is_active'] ) ) { 242 | 243 | if ( version_compare( $plugin['version'], $plugin['current_version'], '>' ) ) { 244 | $plugin['status'] = 'incompatible'; 245 | } else { 246 | $plugin['status'] = 'active'; 247 | } 248 | } else { 249 | 250 | $plugin['status'] = 'inactive'; 251 | 252 | } 253 | 254 | // Add plugin notice for available update. 255 | if ( version_compare( $plugin['new_version'], $plugin['current_version'], '>' ) ) { 256 | 257 | $plugin['update'] = true; 258 | 259 | } 260 | } 261 | 262 | if ( ! $plugin['url'] ) { 263 | 264 | $plugin['url'] = 'https://wordpress.org/plugins/' . $plugin['slug']; 265 | 266 | } 267 | 268 | // Store final, formatted plugin. 269 | $this->plugins[ $plugin['slug'] ] = $plugin; 270 | 271 | } 272 | 273 | /** 274 | * Get data for a plugin, or data for all plugins. 275 | * 276 | * @since 1.0.0 277 | * 278 | * @param string $slug Optional. Slug of plugin to retrieve data for. Leave empty for all plugins. 279 | * @return array|bool Data for all plugins, data for single plugin, or `false` if single plugin doesn't exist. 280 | */ 281 | public function get( $slug = '' ) { 282 | 283 | if ( ! $slug ) { 284 | return $this->plugins; 285 | } 286 | 287 | if ( isset( $this->plugins[ $slug ] ) ) { 288 | return $this->plugins[ $slug ]; 289 | } 290 | 291 | return false; 292 | 293 | } 294 | 295 | /** 296 | * Match installed plugins from WordPress against 297 | * our suggested plugins, to return information 298 | * for our suggested plugins, wnich are currently 299 | * installed. 300 | * 301 | * @since 1.0.0 302 | * 303 | * @return array $installed Installed plugin that are suggested. 304 | */ 305 | public function get_installed() { 306 | 307 | // Create array of our suggested plugin slugs. 308 | $slugs = array(); 309 | 310 | foreach ( $this->pre_plugins as $plugin ) { 311 | $slugs[] = $plugin['slug']; 312 | } 313 | 314 | // Gather installed plugins from our suggested plugins. 315 | $installed = array(); 316 | 317 | $plugins = get_plugins(); 318 | 319 | foreach ( $plugins as $key => $plugin ) { 320 | 321 | $data = get_plugin_data( WP_PLUGIN_DIR . '/' . $key ); 322 | 323 | $slug = dirname( plugin_basename( $key ) ); 324 | 325 | if ( ! in_array( $slug, $slugs ) ) { 326 | continue; 327 | } 328 | 329 | $installed[ $slug ] = array( 330 | 'slug' => $slug, 331 | 'file' => $key, 332 | 'current_version' => $data['Version'], 333 | 'new_version' => $data['Version'], 334 | 'is_active' => is_plugin_active( $key ), 335 | ); 336 | 337 | } 338 | 339 | // Determine which of those installed plugins have an update. 340 | $plugin_updates = get_site_transient( 'update_plugins' ); 341 | 342 | if ( isset( $plugin_updates->response ) ) { 343 | 344 | foreach ( $plugin_updates->response as $plugin ) { 345 | 346 | if ( ! in_array( $plugin->slug, $slugs ) ) { 347 | continue; 348 | } 349 | 350 | $installed[ $plugin->slug ]['new_version'] = $plugin->new_version; 351 | 352 | } 353 | } 354 | 355 | return $installed; 356 | 357 | } 358 | 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-plugin-manager", 3 | "version": "1.0.0", 4 | "description": "A suggested plugin manager drop-in script for WordPress themes and plugins.", 5 | "main": "gulpfile.js", 6 | "scripts": { 7 | "build": "npm run clean && gulp --gulpfile ./build/gulpfile.js", 8 | "build:example": "gulp --gulpfile ./build/gulpfile.js build-example", 9 | "build:php": "gulp --gulpfile ./build/gulpfile.js build-php", 10 | "build:css": "gulp --gulpfile ./build/gulpfile.js build-css", 11 | "build:js:notices": "gulp --gulpfile ./build/gulpfile.js build-js-notices", 12 | "build:js:manager": "gulp --gulpfile ./build/gulpfile.js build-js-manager", 13 | "build:js": "gulp --gulpfile ./build/gulpfile.js build-js", 14 | "clean": "gulp --gulpfile ./build/gulpfile.js clean" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/themeblvd/my-plugin-manager.git" 19 | }, 20 | "author": "Jason Bobich", 21 | "license": "GPL-2.0+", 22 | "bugs": { 23 | "url": "https://github.com/themeblvd/my-plugin-manager/issues" 24 | }, 25 | "homepage": "http://mypluginmanager.com", 26 | "devDependencies": { 27 | "babel-core": "^6.26.3", 28 | "babel-loader": "^7.1.5", 29 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 30 | "babel-preset-env": "^1.7.0", 31 | "del": "^3.0.0", 32 | "gulp": "^3.9.1", 33 | "gulp-autoprefixer": "^5.0.0", 34 | "gulp-clean-css": "^3.9.4", 35 | "gulp-rename": "^1.4.0", 36 | "gulp-replace": "^1.0.0", 37 | "gulp-replace-name": "^1.0.1", 38 | "gulp-sass": "^4.0.1", 39 | "unminified-webpack-plugin": "^2.0.0", 40 | "webpack": "^4.16.2", 41 | "webpack-stream": "^5.0.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/assets/js/modules/actions.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import { updates } from 'wp'; 3 | import { namespace } from '../../../../build-config'; 4 | import { $document } from './vars'; 5 | import { processQueue } from './queue'; 6 | 7 | /** 8 | * Keep track of the current action 9 | * taking place. 10 | * 11 | * @type {string} 12 | */ 13 | let currentAction = null; 14 | 15 | /** 16 | * Set the current action. 17 | * 18 | * @param {string} action Action slug or null. 19 | */ 20 | const setCurrentAction = action => { 21 | currentAction = action; 22 | }; 23 | 24 | /** 25 | * Get the current action. 26 | * 27 | * @return {string} Current action. 28 | */ 29 | const getCurrentAction = () => { 30 | return currentAction; 31 | }; 32 | 33 | /** 34 | * Sends an Ajax request to the server to install a plugin. 35 | * 36 | * @since 1.0.0 37 | * 38 | * @param {object} args Arguments. 39 | * @param {string} args.slug Plugin slug. 40 | * @param {refreshRow=} args.success Optional. Success callback. 41 | * @param {refreshRow=} args.error Optional. Error callback. 42 | * @return {$.promise} A jQuery promise that represents the request, 43 | * decorated with an abort() method. 44 | */ 45 | const installPlugin = args => { 46 | args = { 47 | ...args, 48 | success: refreshRow, 49 | error: refreshRow 50 | }; 51 | 52 | $document.trigger('wp-plugin-installing', args); 53 | 54 | return updates.ajax('install-plugin', args); 55 | }; 56 | 57 | /** 58 | * Sends an Ajax request to the server to update a plugin. 59 | * 60 | * @since 1.0.0 61 | * 62 | * @param {object} args Arguments. 63 | * @param {string} args.plugin Plugin basename. 64 | * @param {string} args.slug Plugin slug. 65 | * @param {refreshRow=} args.success Optional. Success callback. 66 | * @param {refreshRow=} args.error Optional. Error callback. 67 | * @return {$.promise} A jQuery promise that represents the request, 68 | * decorated with an abort() method. 69 | */ 70 | const updatePlugin = args => { 71 | args = { 72 | ...args, 73 | success: refreshRow, 74 | error: refreshRow 75 | }; 76 | 77 | $document.trigger('wp-plugin-updating', args); 78 | 79 | return updates.ajax('update-plugin', args); 80 | }; 81 | 82 | /** 83 | * Sends an Ajax request to the server to delete a plugin. 84 | * 85 | * @since 1.0.0 86 | * 87 | * @param {object} args Arguments. 88 | * @param {string} args.plugin Plugin basename. 89 | * @param {string} args.slug Plugin slug. 90 | * @param {refreshRow=} args.success Optional. Success callback. 91 | * @param {refreshRow=} args.error Optional. Error callback. 92 | * @return {$.promise} A jQuery promise that represents the request, 93 | * decorated with an abort() method. 94 | */ 95 | const deletePlugin = args => { 96 | args = { 97 | ...args, 98 | success: refreshRow, 99 | error: refreshRow 100 | }; 101 | 102 | $document.trigger('wp-plugin-deleting', args); 103 | 104 | return updates.ajax('delete-plugin', args); 105 | }; 106 | 107 | /** 108 | * If the state of a plugin has been succesfully changed, refresh 109 | * its table row in the UI. This includes handling success or error. 110 | * 111 | * @since 1.0.0 112 | * 113 | * @param {object} response Response from the server. 114 | * @param {string} response.slug Slug of the plugin that was deleted. 115 | * @param {string} response.plugin Base name of the plugin that was deleted (only 'delete-plugin'). 116 | * @param {string} response.pluginName Name of the plugin that was deleted. 117 | * @param {string} response.activateUrl URL to activate the just installed plugin (only 'install-plugin'). 118 | * @param {string} response.oldVersion Old version of the plugin (only 'update-plugin'). 119 | * @param {string} response.newVersion New version of the plugin (only 'update-plugin'). 120 | */ 121 | const refreshRow = response => { 122 | const args = { 123 | _ajax_nonce: updates.ajaxNonce, 124 | action: `${namespace}-row-refresh`, 125 | prev_action: getCurrentAction(), 126 | slug: response.slug, 127 | namespace: namespace, 128 | error: null, 129 | error_level: 'error' // `error` or `warning` 130 | }; 131 | 132 | // Handle error message 133 | if (response.errorMessage) { 134 | if ('update-plugin' === args.prev_action && response.debug) { 135 | args.error = response.debug[0]; // More helpful message. 136 | args.error_level = 'warning'; 137 | } else { 138 | args.error = response.errorMessage; 139 | } 140 | 141 | if (response.errorLevel) { 142 | args.error_level = response.errorLevel; 143 | } 144 | } 145 | 146 | const $old_row = $('tr[data-slug="' + args.slug + '"]'); 147 | 148 | // Refresh row from plugin manager. 149 | $.post(ajaxurl, args, response => { 150 | $('#setting-error-plugin-manager-error').remove(); // Remove any lingering notices in page header, from previously finished actions. 151 | 152 | $('.' + args.slug + '-notice').remove(); // Remove notice on current row, if exists. 153 | 154 | $('tr[data-slug="' + args.slug + '"]').replaceWith(response); 155 | 156 | const $new_row = $('tr[data-slug="' + args.slug + '"]'); 157 | 158 | if ($new_row.hasClass('ajax-success')) { 159 | setTimeout(() => { 160 | $new_row.removeClass('install-success update-success delete-success'); 161 | }, 200); 162 | } 163 | 164 | // Clear current action. 165 | setCurrentAction(null); 166 | 167 | // Continue processing any bulk actions, if they exist. 168 | processQueue(); 169 | }); 170 | }; 171 | 172 | /** 173 | * Exports 174 | */ 175 | export { 176 | setCurrentAction, 177 | getCurrentAction, 178 | installPlugin, 179 | updatePlugin, 180 | deletePlugin, 181 | refreshRow 182 | }; 183 | -------------------------------------------------------------------------------- /src/assets/js/modules/queue.js: -------------------------------------------------------------------------------- 1 | import { l10n } from './vars'; 2 | import { setCurrentAction, installPlugin, updatePlugin, deletePlugin, refreshRow } from './actions'; 3 | 4 | /** 5 | * Queue of plugins to bulk process. 6 | * 7 | * @since 1.0.0 8 | * 9 | * @type {array} 10 | */ 11 | const queue = []; 12 | 13 | /** 14 | * When performing bulk actions, this will provide a way to 15 | * iterate through the queue of plugins synchronously to 16 | * take action on. 17 | * 18 | * @since 1.0.0 19 | */ 20 | const processQueue = () => { 21 | if (!queue.length) { 22 | return; 23 | } 24 | 25 | const current = queue[0]; 26 | const action = current.action; 27 | const $row = current.row; 28 | 29 | queue.shift(); 30 | 31 | if ('install-selected' === action) { 32 | $row 33 | .addClass('updating-message') 34 | .find('.row-actions') 35 | .text(l10n.installing); 36 | 37 | setCurrentAction('install-plugin'); 38 | 39 | if ('third-party' === $row.data('source')) { 40 | /* 41 | * We can only install plugins from wordpress.org. 42 | * So, we'll just bypass to refreshing the row 43 | * with an error message. 44 | */ 45 | refreshRow({ 46 | slug: current.slug, 47 | plugin: current.plugin, 48 | errorMessage: l10n.thirdParty, 49 | errorLevel: 'warning' 50 | }); 51 | } else { 52 | installPlugin({ 53 | slug: current.slug 54 | }); 55 | } 56 | } else if ('update-selected' === action) { 57 | $row 58 | .addClass('updating-message') 59 | .find('.row-actions') 60 | .text(l10n.updating); 61 | 62 | setCurrentAction('update-plugin'); 63 | 64 | if ('not-installed' === $row.data('status')) { 65 | /* 66 | * We can't update a plugin that's not installed. So, 67 | * we'll just bypass to refreshing the row with an 68 | * error message. 69 | */ 70 | refreshRow({ 71 | slug: current.slug, 72 | plugin: current.plugin, 73 | errorMessage: l10n.notInstalled, 74 | errorLevel: 'warning' 75 | }); 76 | } else { 77 | updatePlugin({ 78 | slug: current.slug, 79 | plugin: current.plugin 80 | }); 81 | } 82 | } else if ('delete-selected' === action) { 83 | if ('not-installed' == $row.data('status')) { 84 | /* 85 | * If we're deleting a plugin that's not actually 86 | * installed, we'll just refresh the row to clear 87 | * any previous notices. No error message needed. 88 | * 89 | * Note: By not setting currentAction, 90 | * there's no visual success or error animation 91 | * added to the refreshed row. 92 | */ 93 | refreshRow({ 94 | slug: current.slug, 95 | plugin: current.plugin 96 | }); 97 | } else { 98 | $row 99 | .addClass('updating-message') 100 | .find('.row-actions') 101 | .text(l10n.deleting); 102 | 103 | setCurrentAction('delete-plugin'); 104 | 105 | deletePlugin({ 106 | slug: current.slug, 107 | plugin: current.plugin 108 | }); 109 | } 110 | } 111 | }; 112 | 113 | /** 114 | * Exports 115 | */ 116 | export { queue, processQueue }; 117 | -------------------------------------------------------------------------------- /src/assets/js/modules/vars.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import { updates } from 'wp'; 3 | import settings from 'settings'; 4 | 5 | /** 6 | * Cache the jQuery document. 7 | * 8 | * @since 1.0.0 9 | * 10 | * @type {object} 11 | */ 12 | export const $document = $(document); 13 | 14 | /** 15 | * Localized strings from WordPress's updates 16 | * script, merged with any added by the 17 | * plugin manager. 18 | * 19 | * @since 1.0.0 20 | * 21 | * @type {object} 22 | */ 23 | export const l10n = $.extend(settings, updates.l10n); 24 | -------------------------------------------------------------------------------- /src/assets/js/plugin-manager.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import { updates } from 'wp'; 3 | import { namespace } from '../../../build-config'; 4 | import { $document, l10n } from './modules/vars'; 5 | import { queue, processQueue } from './modules/queue'; 6 | import { 7 | setCurrentAction, 8 | installPlugin, 9 | updatePlugin, 10 | deletePlugin, 11 | refreshRow 12 | } from './modules/actions'; 13 | 14 | $document.ready($ => { 15 | const $page = $('#suggested-plugins'); 16 | 17 | /** 18 | * Handles installing a plugin from clicking `.install-now` 19 | * link. 20 | * 21 | * @since 1.0.0 22 | * 23 | * @param {Event} event Event interface. 24 | */ 25 | $page.on('click', '.install-now', function(event) { 26 | event.preventDefault(); 27 | 28 | const $link = $(event.target); 29 | const $row = $link.closest('tr'); 30 | 31 | if ($row.hasClass('updating-message')) { 32 | return; 33 | } 34 | 35 | $row 36 | .addClass('updating-message') 37 | .find('.row-actions') 38 | .text(l10n.installing); 39 | 40 | setCurrentAction('install-plugin'); 41 | 42 | installPlugin({ 43 | slug: $row.data('slug') 44 | }); 45 | }); 46 | 47 | /** 48 | * Handles updating a plugin from clicking `.update-now` 49 | * link. 50 | * 51 | * @since 1.0.0 52 | * 53 | * @param {Event} event Event interface. 54 | */ 55 | $page.on('click', '.update-now', function(event) { 56 | event.preventDefault(); 57 | 58 | const $link = $(event.target); 59 | const $row = $link.closest('tr'); 60 | 61 | if ($row.hasClass('updating-message')) { 62 | return; 63 | } 64 | 65 | $row 66 | .addClass('updating-message') 67 | .find('.row-actions') 68 | .text(l10n.updating); 69 | 70 | setCurrentAction('update-plugin'); 71 | 72 | updatePlugin({ 73 | slug: $row.data('slug'), 74 | plugin: $row.data('plugin') 75 | }); 76 | }); 77 | 78 | /** 79 | * Handles installing a plugin from clicking `.delete-now` 80 | * link. 81 | * 82 | * @since 1.0.0 83 | * 84 | * @param {Event} event Event interface. 85 | */ 86 | $page.on('click', '.delete-now', function(event) { 87 | event.preventDefault(); 88 | 89 | const $link = $(event.target); 90 | const $row = $link.closest('tr'); 91 | 92 | if ($row.hasClass('updating-message')) { 93 | return; 94 | } 95 | 96 | if ( 97 | !window.confirm( 98 | updates.l10n.aysDeleteUninstall.replace('%s', $row.find('.plugin-title strong').text()) 99 | ) 100 | ) { 101 | return; 102 | } 103 | 104 | $row 105 | .addClass('updating-message') 106 | .find('.row-actions') 107 | .text(l10n.deleting); 108 | 109 | setCurrentAction('delete-plugin'); 110 | 111 | deletePlugin({ 112 | slug: $row.data('slug'), 113 | plugin: $row.data('plugin') 114 | }); 115 | }); 116 | 117 | /** 118 | * Handles the submission of the Bulk Actions. 119 | * 120 | * @since 1.0.0 121 | * 122 | * @param {Event} event Event interface. 123 | */ 124 | $page.on('click', '#do-action-top, #do-action-bottom', function(event) { 125 | event.preventDefault(); 126 | 127 | const $button = $(this); 128 | const action = $button.prev('select').val(); 129 | const $form = $button.closest('form'); 130 | 131 | if ('activate-selected' == action || 'deactivate-selected' == action) { 132 | $form.submit(); 133 | return; 134 | } 135 | 136 | $form.find('input[name="checked[]"]:checked').each(function() { 137 | const $checkbox = $(this); 138 | const $row = $checkbox.closest('tr'); 139 | 140 | queue.push({ 141 | row: $row, 142 | slug: $checkbox.val(), 143 | plugin: $row.data('plugin'), 144 | action: action 145 | }); 146 | }); 147 | 148 | $form.find('input[type="checkbox"]').attr('checked', false); 149 | 150 | $form.find('select').val('-1'); 151 | 152 | processQueue(); 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /src/assets/js/plugin-notices.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import { namespace } from '../../../build-config'; 3 | 4 | $(document).ready($ => { 5 | const $notice = $('.plugin-manager-notice'); 6 | 7 | $notice.find('.notice-dismiss').each(function() { 8 | const $el = $(this); 9 | $el.attr('title', $el.closest('.notice').data('dismiss')); 10 | }); 11 | 12 | $notice.on('click', '.notice-dismiss', function() { 13 | const args = { 14 | _ajax_nonce: $notice.data('security'), 15 | action: `${namespace}-dismiss-notice`, 16 | namespace: $notice.data('namespace'), 17 | notice_key: $notice.data('notice-key'), 18 | notice_value: $notice.data('notice-value') 19 | }; 20 | 21 | $.post(ajaxurl, args); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/assets/scss/plugin-manager.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Variables 3 | // 4 | 5 | $clr-success-bg: #ecf7ed; 6 | $clr-delete-bg: #fbeaea; 7 | $clr-incompatible-bg: #fff8e5; 8 | $clr-incompatible-border: #ffb900; 9 | 10 | // 11 | // Table Structure 12 | // 13 | 14 | #suggested-plugins { 15 | .column-compatible-version, 16 | .column-installed-version, 17 | .column-status { 18 | width: 180px; 19 | } 20 | 21 | // Row Marked "Incompatible" 22 | .incompatible td, 23 | .incompatible th { 24 | background-color: $clr-incompatible-bg; 25 | } 26 | .incompatible .column-suggested-version, 27 | .incompatible .column-installed-version, 28 | .incompatible .column-status { 29 | font-weight: bold; 30 | } 31 | .incompatible th.check-column { 32 | border-left: 4px solid $clr-incompatible-border; 33 | } 34 | 35 | // Row Ajax Update 36 | td, 37 | th { 38 | transition: background-color 0.75s ease-in-out; 39 | } 40 | .install-success td, 41 | .install-success th, 42 | .update-success td, 43 | .update-success th { 44 | background-color: $clr-success-bg; 45 | } 46 | .delete-success td, 47 | .delete-success th { 48 | background-color: $clr-delete-bg; 49 | } 50 | 51 | // Row Notices 52 | .row-has-notice td, 53 | .row-has-notice th { 54 | box-shadow: none; 55 | } 56 | .row-notice td { 57 | box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.1); 58 | overflow: hidden; 59 | padding: 0; 60 | } 61 | .row-notice .notice { 62 | margin: 0 20px 15px 40px; 63 | } 64 | .row-notice .notice p:before { 65 | content: '\f534'; 66 | } 67 | 68 | // Row Actions 69 | .row-actions { 70 | line-height: 25px; 71 | } 72 | .updating-message .row-actions:before { 73 | content: '\f463'; 74 | animation: rotation 2s infinite linear; 75 | color: #f56e28; 76 | display: inline-block; 77 | font: normal 20px/1 'dashicons'; 78 | -webkit-font-smoothing: antialiased; 79 | -moz-osx-font-smoothing: grayscale; 80 | margin-right: 2px; 81 | vertical-align: top; 82 | } 83 | .row-actions .no-link { 84 | color: #666; 85 | } 86 | } 87 | 88 | // 89 | // RTL 90 | // 91 | 92 | .rtl #suggested-plugins { 93 | .incompatible th.check-column { 94 | border-left: none; 95 | border-right: 4px solid $clr-incompatible-border; 96 | } 97 | .row-notice .notice { 98 | margin-right: 40px; 99 | margin-left: 20px; 100 | } 101 | .updating-message .row-actions:before { 102 | margin-right: 0; 103 | margin-left: 2px; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/class-my-plugin-manager.php: -------------------------------------------------------------------------------- 1 | Add New` screen. 56 | * 57 | * @since 1.0.0 58 | */ 59 | class _My_Plugin_Manager { 60 | 61 | /** 62 | * Optional class options. 63 | * 64 | * @since 1.0.0 65 | * @see set_args() 66 | * @var array 67 | */ 68 | private $args = array(); 69 | 70 | /** 71 | * Information for package where drop-in is contained. 72 | * 73 | * @since 1.0.0 74 | * @see set_package() 75 | * @var array 76 | */ 77 | private $package; 78 | 79 | /** 80 | * Formatted plugins object. 81 | * 82 | * @since 1.0.0 83 | * @var My_Plugins 84 | */ 85 | private $plugins = array(); 86 | 87 | /** 88 | * Admin screen base ID to match against 89 | * get_current_screen(). 90 | * 91 | * @since 1.0.0 92 | * @var string 93 | */ 94 | private $admin_screen_base; 95 | 96 | /** 97 | * Class constructor. 98 | * 99 | * @since 1.0.0 100 | * 101 | * @param array $plugins Initial, unformatted suggested plugins. 102 | * @param array $args { 103 | * Optional. Overriding class options. 104 | * 105 | * @type string $page_title Title for admin page. 106 | * @type string $views_title Title used for subtle link on Plugins page. 107 | * @type string $extended_title A more descriptive title. 108 | * @type string $tab_title Title for plugin installer tab. 109 | * @type string $menu_title Title for admin sidebar menu. 110 | * @type string $menu_slug Slug used for admin page URL. 111 | * @type string $capability User capability for accessing admin page. 112 | * } 113 | */ 114 | public function __construct( $plugins, $args = array() ) { 115 | 116 | /* 117 | * Temporarily store $args before set_args(). 118 | * 119 | * We need the raw data for set_package(), even 120 | * though it's not all formatted yet. 121 | */ 122 | $this->args = $args; 123 | 124 | /* 125 | * Setup an array of information for the package. 126 | * 127 | * This information also includes figuring the URL and 128 | * path to the drop-in directory, which is needs to happen 129 | * before any files or assets are included. 130 | */ 131 | $this->set_package(); 132 | 133 | // Setup plugins object. 134 | $this->set_plugins( $plugins ); 135 | 136 | // Merge optional arguments with defaults. 137 | $this->set_args( $args ); 138 | 139 | // Setup admin-wide plugin notices. 140 | $this->set_notices(); 141 | 142 | // Add the admin page to manage plugins. 143 | add_action( 'admin_menu', array( $this, 'add_page' ) ); 144 | 145 | // Add the admin page to manage plugins. 146 | add_action( 'admin_enqueue_scripts', array( $this, 'add_assets' ) ); 147 | 148 | // Adds a link to our admin page, from the Installed Plugins screen. 149 | add_filter( 'views_plugins', array( $this, 'add_plugins_view' ) ); 150 | 151 | // Adds a link to our admin page, from Plugin Install screen. 152 | add_filter( 'views_plugin-install', array( $this, 'add_install_view' ) ); 153 | 154 | // Hook Ajax requests. 155 | add_action( 'wp_ajax_my_namespace-row-refresh', array( $this, 'row_refresh' ) ); 156 | 157 | // Hook non-Ajax requests. 158 | add_action( 'current_screen', array( $this, 'request' ) ); 159 | 160 | } 161 | 162 | /** 163 | * Sets an array of information for the current package 164 | * containing the drop-in directory. 165 | * 166 | * By default, the package info is generated, assuming 167 | * this class exists within a parent theme. 168 | * 169 | * For child themes: Pass in `$args['child_theme'] = true` 170 | * when instantiating object. 171 | * 172 | * For plugins: Pass in `$args['plugin_file'] = __FILE__` 173 | * when instantiating object. 174 | * 175 | * @since 1.0.0 176 | */ 177 | private function set_package() { 178 | 179 | $this->package = array( 180 | 'directory' => dirname( __FILE__ ), 181 | ); 182 | 183 | if ( ! empty( $this->args['plugin_file'] ) ) { 184 | 185 | /* 186 | * Setup package info when drop-in is included 187 | * within plugin. 188 | */ 189 | $plugin_file = $this->args['plugin_file']; 190 | 191 | $plugin_slug = explode( '/', plugin_basename( $plugin_file ) ); 192 | 193 | $this->package['slug'] = $plugin_slug[0]; 194 | 195 | $plugin = get_file_data( $plugin_file, array( 196 | 'Name' => 'Plugin Name' 197 | ), 'plugin' ); 198 | 199 | $this->package['name'] = $plugin['Name']; 200 | 201 | /* 202 | * Dynamically set the URI and file path to the 203 | * drop-in directory within the plugin. 204 | * 205 | * This allows the plugin author to place the drop-in 206 | * directory anywhere within their theme. 207 | */ 208 | $plugin_dir = plugin_dir_path( $plugin_file ); 209 | 210 | $dropin_path = str_replace( $plugin_dir, '', $this->package['directory'] ); 211 | 212 | $this->package['url'] = plugin_dir_url( $plugin_file ) . untrailingslashit( $dropin_path ); 213 | 214 | } else { 215 | 216 | /* 217 | * Setup package info when drop-in is included 218 | * within theme. 219 | */ 220 | if ( ! empty( $this->args['child_theme'] ) ) { 221 | $theme = wp_get_theme( get_stylesheet() ); 222 | } else { 223 | $theme = wp_get_theme( get_template() ); 224 | } 225 | 226 | $this->package['slug'] = $theme->get_stylesheet(); 227 | 228 | $this->package['name'] = $theme->get( 'Name' ); 229 | 230 | /* 231 | * Dynamically set the URI and file path to the 232 | * drop-in directory within the theme. 233 | * 234 | * This allows the theme author to place the drop-in 235 | * directory anywhere within their theme. 236 | */ 237 | $theme_dir = $theme->get_stylesheet_directory(); 238 | 239 | $dropin_path = str_replace( $theme_dir, '', $this->package['directory'] ); 240 | 241 | $this->package['url'] = $theme->get_stylesheet_directory_uri() . untrailingslashit( $dropin_path ); 242 | 243 | } 244 | 245 | } 246 | 247 | /** 248 | * Setup plugins object. 249 | * 250 | * @since 1.0.0 251 | * 252 | * @param array $plugins Initial, unformatted plugins passed to plugin manager. 253 | */ 254 | private function set_plugins( $plugins ) { 255 | 256 | /** 257 | * Include _My_Plugins class to store 258 | * plugin data. 259 | */ 260 | require_once( $this->package['directory'] . '/class-my-plugins.php' ); 261 | 262 | // Setup plugins. 263 | $this->plugins = new _My_Plugins( $plugins, $this ); 264 | 265 | } 266 | 267 | /** 268 | * Set and format any arguments passed in, to override 269 | * defaults. 270 | * 271 | * @since 1.0.0 272 | * 273 | * @param array $args { 274 | * Any argument overrides passed to the class. 275 | * 276 | * @type string $page_title Title of plugin manager page. 277 | * @type string $views_title Text for link to the plugin manager admin page, which gets inserted at WordPress's main Plugins admin screen. Set to `false` to disable. 278 | * @type string $tab_title Text for link to the plugin manager admin page, which gets inserted at WordPress's Install Plugins admin screen. Set to `false` to disable. 279 | * @type string $extended_title An extended title that gets used in the title tags of links generated from both `$views_title` and `$tab_title`. 280 | * @type string $menu_title Title of plugin manager menu item. 281 | * @type string $parent_slug Parent url slug for plugin manager admin page. 282 | * @type string $menu_slug URL slug for the plugin manager admin page. 283 | * @type string $capability WordPress user capability for plugin manager admin page. 284 | * @type string $nag_action Text of the link, which leads the user to the plugin manager admin page. 285 | * @type string $nag_dimiss Screen reader text to dismiss plugin manager nags. 286 | * @type string $nag_update Message to tell users not all plugins are compatible. 287 | * @type string $nag_install_single Message to install suggested plugins, if only one exists. 288 | * @type string $nag_install_multiple Message to install suggested plugins. 289 | * @type bool $child_theme Whether implementing from a child theme. Required if implementing from a child theme. 290 | * @type string $plugin_file Absolute path to project plugin's main file. Required if implementing from a plugin. 291 | * } 292 | */ 293 | private function set_args( $args = array() ) { 294 | 295 | $this->args = wp_parse_args( $args, array( 296 | 'page_title' => __( 'Suggested Plugins', 'my-text-domain' ), 297 | // translators: 1: name of theme 298 | 'views_title' => sprintf( __( 'Suggested by %s', 'my-text-domain' ), $this->package['name'] ), 299 | // translators: 1: name of theme 300 | 'tab_title' => sprintf( __( 'Suggested by %s', 'my-text-domain' ), $this->package['name'] ), 301 | // translators: 1: name of theme 302 | 'extended_title' => sprintf( __( 'Suggested Plugins by %s', 'my-text-domain' ), $this->package['name'] ), 303 | 'menu_title' => '', // Takes on page_title, when left blank. 304 | 'parent_slug' => 'themes.php', 305 | 'menu_slug' => 'suggested-plugins', 306 | 'capability' => 'install_plugins', 307 | 'nag_action' => __( 'Manage suggested plugins', 'my-text-domain' ), 308 | 'nag_dismiss' => __( 'Dismiss this notice', 'my-text-domain' ), 309 | // translators: 1: name of theme 310 | 'nag_update' => __( 'Not all of your active, suggested plugins are compatible with %s.', 'my-text-domain' ), 311 | // translators: 1: name of theme, 2: number of suggested plugins 312 | 'nag_install_single' => __( '%1$s suggests installing %2$s plugin.', 'my-text-domain' ), 313 | // translators: 1: name of theme, 2: number of suggested plugins 314 | 'nag_install_multiple' => __( '%1$s suggests installing %2$s plugins.', 'my-text-domain' ), 315 | 'child_theme' => false, 316 | 'plugin_file' => '', 317 | )); 318 | 319 | if ( ! $this->args['menu_title'] ) { 320 | $this->args['menu_title'] = $this->args['page_title']; 321 | } 322 | 323 | } 324 | 325 | /** 326 | * Setup admin-wide notices regarding suggested 327 | * plugins. 328 | * 329 | * @since 1.0.0 330 | */ 331 | private function set_notices() { 332 | 333 | /** 334 | * Include _My_Plugin_Notices class to setup 335 | * and display admin-wide notices, if they're needed. 336 | */ 337 | require_once( $this->package['directory'] . '/class-my-plugin-notices.php' ); 338 | 339 | // Setup notices. 340 | $args = array( 341 | 'package_name' => $this->package['name'], 342 | 'package_url' => $this->package['url'], 343 | 'admin_url' => $this->get_admin_url(), 344 | 'nag_action' => $this->args['nag_action'], 345 | 'nag_dismiss' => $this->args['nag_dismiss'], 346 | 'nag_update' => $this->args['nag_update'], 347 | 'nag_install_single' => $this->args['nag_install_single'], 348 | 'nag_install_multiple' => $this->args['nag_install_multiple'], 349 | ); 350 | 351 | $notices = new _My_Plugin_Notices( $args, $this, $this->plugins ); 352 | 353 | } 354 | 355 | /** 356 | * Add the suggsted plugin admin page to manage 357 | * plugins. 358 | * 359 | * @since 1.0.0 360 | */ 361 | public function add_page() { 362 | 363 | $this->admin_screen_base = add_theme_page( 364 | $this->args['page_title'], 365 | $this->args['menu_title'], 366 | $this->args['capability'], 367 | $this->args['menu_slug'], 368 | array( $this, 'display_page' ) 369 | ); 370 | 371 | } 372 | 373 | /** 374 | * Add any CSS or JavaScript to plugin manager 375 | * admin page. 376 | * 377 | * @since 1.0.0 378 | */ 379 | public function add_assets() { 380 | 381 | /* 382 | * The plugin-manager .js and .css files should only 383 | * be included on our specific admin page for managing 384 | * plugins. 385 | */ 386 | if ( ! $this->is_admin_screen() ) { 387 | return; 388 | } 389 | 390 | $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; 391 | 392 | wp_enqueue_script( 'updates' ); 393 | 394 | wp_enqueue_script( 395 | 'my_namespace-plugin-manager', 396 | esc_url( $this->package['url'] . "/assets/js/plugin-manager$suffix.js" ), 397 | array( 'jquery', 'updates' ) 398 | ); 399 | 400 | wp_localize_script( 401 | 'my_namespace-plugin-manager', 402 | 'pluginManagerSettings', 403 | array( 404 | 'thirdParty' => __( 'Only plugins from wordpress.org can be installed directly here.', 'my-text-domain' ), 405 | 'notInstalled' => __( 'Plugin update skipped because it is not installed.', 'my-text-domain' ), 406 | ) 407 | ); 408 | 409 | wp_enqueue_style( 410 | 'my_namespace-plugin-manager', 411 | esc_url( $this->package['url'] . "/assets/css/plugin-manager$suffix.css" ) 412 | ); 413 | 414 | } 415 | 416 | /** 417 | * Display the suggsted plugin admin page to 418 | * manage plugins. 419 | * 420 | * @since 1.0.0 421 | */ 422 | public function display_page() { 423 | 424 | $plugins = $this->plugins->get(); 425 | 426 | settings_errors( 'plugin-manager' ); 427 | 428 | ?> 429 |
430 | 431 |

args['page_title'] ); ?>

432 | 433 | 434 | 435 |
436 | 437 | display_table_nav( 'top' ); ?> 438 | 439 | 440 | 441 | display_table_header( 'thead' ); ?> 442 | 443 | 444 | 445 | 446 | 447 | display_table_row( $plugin ); ?> 448 | 449 | 450 | 451 | 452 | 453 | display_table_header( 'tfoot' ); ?> 454 | 455 |
456 | 457 | display_table_nav( 'bottom' ); ?> 458 | 459 |
460 | 461 | 462 | 463 |

464 | 465 | 466 | 467 |
468 | __( 'Install', 'my-text-domain' ), 483 | 'activate-selected' => __( 'Activate', 'my-text-domain' ), 484 | 'deactivate-selected' => __( 'Deactivate', 'my-text-domain' ), 485 | 'update-selected' => __( 'Update', 'my-text-domain' ), 486 | 'delete-selected' => __( 'Delete', 'my-text-domain' ), 487 | ); 488 | 489 | if ( 'top' === $which ) { 490 | wp_nonce_field( 'bulk-plugins' ); 491 | } 492 | 493 | ?> 494 |
495 |
496 | 497 | 500 | 501 | 512 | 513 | "do-action-$which", 517 | ) 518 | ); 519 | ?> 520 | 521 |
522 |
523 | 539 | <> 540 | 541 | 542 | 543 | 544 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | > 569 | 600 | 601 | 602 | 603 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 |
619 | display_actions( $plugin ); ?> 620 |
621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | display_status( $plugin ); ?> 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 |
650 |

651 |
652 | 653 | 654 | 655 | 656 | 657 | 658 | $plugin['url'], 687 | 'text' => __( 'Details', 'my-text-domain' ), 688 | // translators: 1: name of plugin 689 | 'label' => sprintf( __( 'More Information about %s', 'my-text-domain' ), $plugin['name'] ), 690 | 'target' => '_blank', 691 | 'nonce' => null, 692 | ); 693 | 694 | /* 695 | if ( $is_wp ) { 696 | $actions['details']['target'] = '_self'; 697 | $actions['details']['class'] = 'thickbox open-plugin-details-modal'; 698 | } 699 | */ 700 | } 701 | 702 | /* 703 | * Add "Install" or "Get Plugin" link. 704 | * 705 | * "Install" - If the plugin isn't installed and it's 706 | * located in the wordpress.org repo, give the user 707 | * an install link. 708 | * 709 | * "Get Plugin" - For a plugin not installed from 710 | * another source, give them an external URL to get 711 | * the plugin. 712 | */ 713 | if ( 'not-installed' === $plugin['status'] ) { 714 | 715 | if ( $is_wp ) { 716 | 717 | /* 718 | * This URL is a fallback for when JavaScript is broken 719 | * or disabled. It will lead to /wp-admin/update.php and 720 | * trigger default action of install a plugin. 721 | */ 722 | $url = add_query_arg( 723 | array( 724 | 'action' => 'install-plugin', 725 | 'plugin' => urlencode( $plugin['slug'] ), 726 | '_wpnonce' => wp_create_nonce( 'install-plugin_' . $plugin['slug'] ), // Formatted for WP's update.php. 727 | ), 728 | self_admin_url( 'update.php' ) 729 | ); 730 | 731 | $actions['install'] = array( 732 | 'url' => $url, 733 | 'text' => __( 'Install', 'my-text-domain' ), 734 | // translators: 1: name of plugin 735 | 'label' => sprintf( __( 'Install %s', 'my-text-domain' ), $plugin['name'] ), 736 | 'target' => '_self', 737 | 'nonce' => null, // No nonce needed, using wp.updates.ajaxNonce. 738 | ); 739 | 740 | } else { 741 | 742 | $actions['install'] = array( 743 | 'url' => $plugin['url'], 744 | 'text' => __( 'Get Plugin', 'my-text-domain' ), 745 | // translators: 1: name of plugin 746 | 'label' => sprintf( __( 'Install %s', 'my-text-domain' ), $plugin['name'] ), 747 | 'target' => '_blank', 748 | 'nonce' => null, 749 | ); 750 | 751 | } 752 | } 753 | 754 | // Add "Activate" or "Deactivate" link. 755 | if ( 'inactive' === $plugin['status'] ) { 756 | 757 | $url = add_query_arg( 758 | array( 759 | 'action' => 'activate', 760 | 'plugin' => urlencode( $plugin['slug'] ), 761 | '_wpnonce' => wp_create_nonce( 'plugin-request_' . $plugin['file'] ), 762 | ), 763 | $this->get_admin_url() 764 | ); 765 | 766 | $actions['activate'] = array( 767 | 'url' => $url, 768 | 'text' => __( 'Activate', 'my-text-domain' ), 769 | // translators: 1: name of plugin 770 | 'label' => sprintf( __( 'Activate %s', 'my-text-domain' ), $plugin['name'] ), 771 | 'target' => '_self', 772 | ); 773 | 774 | } elseif ( 'active' === $plugin['status'] || 'incompatible' === $plugin['status'] ) { 775 | 776 | $url = add_query_arg( 777 | array( 778 | 'action' => 'deactivate', 779 | 'plugin' => urlencode( $plugin['slug'] ), 780 | '_wpnonce' => wp_create_nonce( 'plugin-request_' . $plugin['file'] ), 781 | ), 782 | $this->get_admin_url() 783 | ); 784 | 785 | $actions['deactivate'] = array( 786 | 'url' => $url, 787 | 'text' => __( 'Deactivate', 'my-text-domain' ), 788 | // translators: 1: name of plugin 789 | 'label' => sprintf( __( 'Deactivate %s', 'my-text-domain' ), $plugin['name'] ), 790 | 'target' => '_self', 791 | 'nonce' => wp_create_nonce( 'plugin-activation_' . $plugin['slug'] ), 792 | ); 793 | 794 | } 795 | 796 | // Add "Delete" link. 797 | if ( 'inactive' === $plugin['status'] ) { 798 | 799 | /* 800 | * This URL is a fallback for when JavaScript is broken 801 | * or disabled. It will lead to /wp-admin/plugins.php and 802 | * trigger default action of deleting a plugin. 803 | */ 804 | $url = add_query_arg( 805 | array( 806 | 'action' => 'delete-selected', 807 | 'verify-delete' => '1', 808 | 'checked[]' => urlencode( $plugin['file'] ), 809 | '_wpnonce' => wp_create_nonce( 'bulk-plugins' ), // Formatted for plugins.php 810 | ), 811 | self_admin_url( 'plugins.php' ) 812 | ); 813 | 814 | $actions['delete'] = array( 815 | 'url' => $url, 816 | 'text' => __( 'Delete', 'my-text-domain' ), 817 | // translators: 1: name of plugin 818 | 'label' => sprintf( __( 'Delete %s', 'my-text-domain' ), $plugin['name'] ), 819 | 'target' => '_self', 820 | 'nonce' => wp_create_nonce( 'delete-plugin_' . $plugin['slug'] ), 821 | ); 822 | 823 | } 824 | 825 | // Add "Update to version {version}" link or "Plugin is up-to-date" message. 826 | if ( $plugin['update'] ) { 827 | 828 | /* 829 | * This URL is a fallback for when JavaScript is broken 830 | * or disabled. It will lead to /wp-admin/update.php and 831 | * trigger default action of updating a plugin. 832 | */ 833 | $url = add_query_arg( 834 | array( 835 | 'action' => 'upgrade-plugin', 836 | 'plugin' => urlencode( $plugin['file'] ), 837 | '_wpnonce' => wp_create_nonce( 'upgrade-plugin_' . $plugin['file'] ), // Formatted for update.php 838 | ), 839 | self_admin_url( 'update.php' ) 840 | ); 841 | 842 | $actions['update'] = array( 843 | 'url' => $url, 844 | 'text' => sprintf( 845 | // translators: 1: new version of plugin 846 | __( 'Update to %s', 'my-text-domain' ), 847 | $plugin['new_version'] 848 | ), 849 | 'label' => sprintf( 850 | // translators: 1: name of plugin, 2: new version of plugin 851 | __( 'Update %1$s to version %2$s', 'my-text-domain' ), 852 | $plugin['name'], 853 | $plugin['new_version'] 854 | ), 855 | 'target' => '_self', 856 | 'nonce' => null, // No nonce needed, using wp.updates.ajaxNonce. 857 | ); 858 | 859 | } elseif ( 'not-installed' !== $plugin['status'] ) { 860 | 861 | $actions['update'] = array( 862 | 'text' => __( 'Plugin is up-to-date', 'my-text-domain' ), 863 | ); 864 | 865 | } 866 | 867 | // Build $output from $actions array data. 868 | $output = array(); 869 | 870 | foreach ( $actions as $key => $action ) { 871 | 872 | if ( ! empty( $action['url'] ) ) { 873 | 874 | $class = ''; 875 | 876 | if ( false !== strpos( $action['url'], get_site_url() ) ) { 877 | $class = $key . '-now'; 878 | } 879 | 880 | if ( ! empty( $action['class'] ) ) { 881 | $class .= ' ' . $action['class']; 882 | } 883 | 884 | $item = sprintf( 885 | '%s', 886 | $key, 887 | esc_url( $action['url'] ), 888 | esc_attr( $class ), 889 | esc_attr( $action['label'] ), 890 | esc_attr( $action['target'] ), 891 | esc_html( $action['text'] ) 892 | ); 893 | 894 | if ( ! empty( $action['nonce'] ) ) { 895 | 896 | $item = str_replace( 897 | 'href', 898 | sprintf( 'data-ajax-nonce="%s" href', $action['nonce'] ), 899 | $item 900 | ); 901 | 902 | } 903 | } else { 904 | 905 | $item = sprintf( 906 | '%s', 907 | $key, 908 | esc_html( $action['text'] ) 909 | ); 910 | 911 | } 912 | 913 | $output[] = $item; 914 | 915 | } 916 | 917 | echo implode( ' | ', $output ); 918 | 919 | } 920 | 921 | /** 922 | * Display the status of a plugin. 923 | * 924 | * For more on how status is determined see docs 925 | * for set_plugins() method. 926 | * 927 | * @since 1.0.0 928 | * 929 | * @param array $plugin Plugin data. 930 | */ 931 | private function display_status( $plugin ) { 932 | 933 | switch ( $plugin['status'] ) { 934 | case 'active': 935 | esc_html_e( 'Active', 'my-text-domain' ); 936 | break; 937 | 938 | case 'incompatible': 939 | esc_html_e( 'Incompatible', 'my-text-domain' ); 940 | break; 941 | 942 | case 'inactive': 943 | esc_html_e( 'Installed', 'my-text-domain' ); 944 | break; 945 | 946 | default: 947 | esc_html_e( 'Not Installed', 'my-text-domain' ); 948 | } 949 | 950 | } 951 | 952 | /** 953 | * Adds a link to Plugins screen that links 954 | * to our plugin manager admin page. 955 | * 956 | * @since 1.0.0 957 | */ 958 | public function add_plugins_view( $views ) { 959 | 960 | if ( ! $this->args['views_title'] ) { 961 | return $views; 962 | } 963 | 964 | $plugins = $this->plugins->get(); 965 | 966 | if ( $plugins ) { 967 | 968 | $views['suggested'] = sprintf( 969 | "%s (%d)", 970 | esc_url( $this->get_admin_url() ), 971 | esc_html( $this->args['extended_title'] ), 972 | esc_html( $this->args['views_title'] ), 973 | count( $plugins ) 974 | ); 975 | 976 | } 977 | 978 | return $views; 979 | 980 | } 981 | 982 | /** 983 | * Add tab to Plugin Installer screen that links 984 | * to our plugin manager admin page. 985 | * 986 | * @since 1.0.0 987 | */ 988 | public function add_install_view( $tabs ) { 989 | 990 | if ( ! $this->args['tab_title'] ) { 991 | return $tabs; 992 | } 993 | 994 | if ( $this->plugins->get() ) { 995 | 996 | $tabs['suggested-by-theme'] = sprintf( 997 | "%s", 998 | esc_url( $this->get_admin_url() ), 999 | esc_html( $this->args['extended_title'] ), 1000 | esc_html( $this->args['tab_title'] ) 1001 | ); 1002 | 1003 | } 1004 | 1005 | return $tabs; 1006 | 1007 | } 1008 | 1009 | /** 1010 | * Get the URL to the admin page to manage plugins. 1011 | * 1012 | * @since 1.0.0 1013 | * 1014 | * @return string URL to plugin-manager admin page. 1015 | */ 1016 | public function get_admin_url() { 1017 | 1018 | return add_query_arg( 1019 | 'page', 1020 | $this->args['menu_slug'], 1021 | admin_url( $this->args['parent_slug'] ) 1022 | ); 1023 | 1024 | } 1025 | 1026 | /** 1027 | * Get source to display for a plugin. 1028 | * 1029 | * @since 1.0.0 1030 | * 1031 | * @param array $plugin Plugin data. 1032 | * @return string Plugin source, `wordpress.org` or `third-party`. 1033 | */ 1034 | public function get_plugin_source( $plugin ) { 1035 | 1036 | if ( false === strpos( $plugin['url'], 'wordpress.org' ) ) { 1037 | return 'third-party'; 1038 | } 1039 | 1040 | return 'wordpress.org'; 1041 | 1042 | } 1043 | 1044 | /** 1045 | * Check whether we're currently on the plugin 1046 | * manager admin page or not. 1047 | * 1048 | * @since 1.0.0 1049 | * 1050 | * @return bool If plugin-manager admin page. 1051 | */ 1052 | public function is_admin_screen() { 1053 | 1054 | $screen = get_current_screen(); 1055 | 1056 | if ( $screen && $this->admin_screen_base === $screen->base ) { 1057 | return true; 1058 | } 1059 | 1060 | return false; 1061 | 1062 | } 1063 | 1064 | /** 1065 | * Handle Ajax request when a plugin's status has 1066 | * changed and its table row needs to be refreshed. 1067 | * 1068 | * @since 1.0.0 1069 | */ 1070 | public function row_refresh() { 1071 | 1072 | check_ajax_referer( 'updates' ); 1073 | 1074 | if ( ! $this->plugins->is_set() ) { 1075 | $this->plugins->set(); 1076 | } 1077 | 1078 | $plugin = $this->plugins->get( $_POST['slug'] ); 1079 | 1080 | if ( $plugin ) { 1081 | 1082 | if ( ! empty( $_POST['error'] ) ) { 1083 | 1084 | $plugin['notice'] = array( 1085 | 'class' => 'notice-' . $_POST['error_level'], 1086 | 'message' => $_POST['error'], 1087 | ); 1088 | 1089 | $plugin['row-class'] = array( 'row-has-notice' ); 1090 | 1091 | } else { 1092 | 1093 | $plugin['row-class'] = array( 'ajax-success' ); 1094 | 1095 | if ( ! empty( $_POST['prev_action'] ) ) { 1096 | 1097 | $action_slug = str_replace( '-plugin', '', $_POST['prev_action'] ); 1098 | 1099 | $plugin['row-class'][] = $action_slug . '-success'; 1100 | 1101 | } 1102 | } 1103 | 1104 | $this->display_table_row( $plugin ); 1105 | 1106 | } 1107 | 1108 | wp_die(); 1109 | 1110 | } 1111 | 1112 | /** 1113 | * Handles any non-Ajax request. 1114 | * 1115 | * We use this for activating and deactivating plugins 1116 | * because these actions require that the WordPress 1117 | * admin is refreshed. So handling them through Ajax 1118 | * is a bit unnecessary. 1119 | * 1120 | * This method handles both the activation and 1121 | * deactivation processes, along with error message 1122 | * handling and success message on redirect. 1123 | * 1124 | * Also, single plugin and bulk processing is supported. 1125 | * 1126 | * @since 1.0.0 1127 | */ 1128 | public function request() { 1129 | 1130 | global $_REQUEST; 1131 | 1132 | // Do nothing if this isn't our admin screen. 1133 | if ( ! $this->is_admin_screen() ) { 1134 | return; 1135 | } 1136 | 1137 | // Check for bulk actions. 1138 | $do_bulk = false; 1139 | 1140 | if ( isset( $_REQUEST['action-top'] ) || isset( $_REQUEST['action-bottom'] ) ) { 1141 | 1142 | $do_bulk = true; 1143 | 1144 | if ( ! empty( $_REQUEST['action-top'] ) && -1 != $_REQUEST['action-top'] ) { 1145 | 1146 | $_REQUEST['action'] = $_REQUEST['action-top']; 1147 | 1148 | } elseif ( ! empty( $_REQUEST['action-bottom'] ) && -1 != $_REQUEST['action-bottom'] ) { 1149 | 1150 | $_REQUEST['action'] = $_REQUEST['action-bottom']; 1151 | 1152 | } 1153 | } 1154 | 1155 | if ( empty( $_REQUEST['action'] ) ) { 1156 | return; 1157 | } 1158 | 1159 | $_SERVER['REQUEST_URI'] = remove_query_arg( 1160 | array( 1161 | 'action', 1162 | 'success', 1163 | '_error_nonce', 1164 | ), 1165 | $_SERVER['REQUEST_URI'] 1166 | ); 1167 | 1168 | /* 1169 | * If this was a redirect from a successful activate 1170 | * or deactivate action, we'll just add a success 1171 | * message and then return. 1172 | */ 1173 | if ( isset( $_REQUEST['success'] ) ) { 1174 | 1175 | $message = ''; 1176 | 1177 | if ( 'activate' === $_REQUEST['action'] ) { 1178 | 1179 | $message = __( 'Plugin activated.', 'my-text-domain' ); 1180 | 1181 | } elseif ( 'activate-selected' === $_REQUEST['action'] ) { 1182 | 1183 | $num = $_REQUEST['success']; 1184 | 1185 | $message = sprintf( 1186 | // translators: 1: number of plugins activated without error 1187 | _n( 1188 | '%s plugin activated successfully.', 1189 | '%s plugins activated successfully.', 1190 | $num, 1191 | 'my-text-domain' 1192 | ), 1193 | $num 1194 | ); 1195 | 1196 | } elseif ( 'deactivate' === $_REQUEST['action'] ) { 1197 | 1198 | $message = __( 'Plugin deactivated.', 'my-text-domain' ); 1199 | 1200 | } elseif ( 'deactivate-selected' === $_REQUEST['action'] ) { 1201 | 1202 | $message = __( 'Plugins deactivated.', 'my-text-domain' ); 1203 | 1204 | } 1205 | 1206 | if ( $message ) { 1207 | 1208 | add_settings_error( 1209 | 'plugin-manager', 1210 | 'plugin-manager-error', 1211 | $message, 1212 | 'updated' 1213 | ); 1214 | 1215 | } 1216 | 1217 | return; 1218 | 1219 | } 1220 | 1221 | // Get all plugin data. 1222 | $plugins_data = $this->plugins->get(); 1223 | 1224 | /* 1225 | * Perform error checking, to see if we have everything 1226 | * to get started activating or deactivating plugins. 1227 | */ 1228 | $error = ''; 1229 | 1230 | if ( $do_bulk && empty( $_REQUEST['checked'] ) ) { 1231 | 1232 | $error = __( 'No plugins were selected.', 'my-text-domain' ); 1233 | 1234 | } elseif ( ! $do_bulk && empty( $_REQUEST['plugin'] ) ) { 1235 | 1236 | $error = __( 'No plugin slug was given.', 'my-text-domain' ); 1237 | 1238 | } elseif ( ! current_user_can( 'update_plugins' ) ) { 1239 | 1240 | $error = __( 'Sorry, you are not allowed to update plugins for this site.', 'my-text-domain' ); 1241 | 1242 | } elseif ( ! $do_bulk && empty( $plugins_data[ $_REQUEST['plugin'] ] ) ) { 1243 | 1244 | $error = sprintf( 1245 | // translators: 1: slug of plugin being activated 1246 | __( 'The plugin %s doesn\'t exist within the plugin manager\'s registered plugins.', 'my-text-domain' ), 1247 | $_REQUEST['plugin'] 1248 | ); 1249 | 1250 | } 1251 | 1252 | /* 1253 | * If no errors were found above, we can begin activating 1254 | * or deactivating plugins. 1255 | */ 1256 | if ( ! $error ) { 1257 | 1258 | $success = 0; // Count of how many successful plugins activated on bulk. 1259 | 1260 | $redirect = add_query_arg( 1261 | array( 1262 | 'action' => $_REQUEST['action'], 1263 | 'success' => 1, 1264 | ), 1265 | $this->get_admin_url() 1266 | ); 1267 | 1268 | if ( $do_bulk ) { 1269 | 1270 | $plugins = array(); 1271 | 1272 | foreach ( $_REQUEST['checked'] as $slug ) { 1273 | 1274 | if ( ! empty( $plugins_data[ $slug ] ) ) { 1275 | 1276 | $plugins[ $slug ] = $plugins_data[ $slug ]; 1277 | 1278 | } 1279 | } 1280 | 1281 | if ( ! $plugins ) { 1282 | $error = __( 'No valid plugins given for bulk action.', 'my-text-domain' ); 1283 | } 1284 | } else { 1285 | 1286 | $plugin = $plugins_data[ $_REQUEST['plugin'] ]; 1287 | 1288 | } 1289 | 1290 | // Perform action. 1291 | if ( 'activate' === $_REQUEST['action'] ) { 1292 | 1293 | $result = activate_plugin( $plugin['file'], $redirect ); 1294 | 1295 | } elseif ( 'activate-selected' === $_REQUEST['action'] ) { 1296 | 1297 | foreach ( $plugins as $plugin ) { 1298 | 1299 | $result = activate_plugin( $plugin['file'] ); 1300 | 1301 | if ( ! is_wp_error( $result ) ) { 1302 | $success++; 1303 | } 1304 | } 1305 | 1306 | if ( $success ) { // At least one plugin needed to be successful. 1307 | 1308 | $redirect = add_query_arg( 'success', $success, $redirect ); 1309 | 1310 | wp_redirect( $redirect ); 1311 | 1312 | } else { 1313 | 1314 | $error = __( 'None of the selected plugins could be activated. Make sure they are installed.', 'my-text-domain' ); 1315 | 1316 | } 1317 | } elseif ( 'deactivate' === $_REQUEST['action'] || 'deactivate-selected' === $_REQUEST['action'] ) { 1318 | 1319 | $deactivate = array(); 1320 | 1321 | if ( $do_bulk ) { 1322 | 1323 | foreach ( $plugins as $plugin ) { 1324 | $deactivate[] = trailingslashit( WP_PLUGIN_DIR ) . $plugin['file']; 1325 | } 1326 | } else { 1327 | 1328 | $deactivate[] = trailingslashit( WP_PLUGIN_DIR ) . $plugin['file']; 1329 | 1330 | } 1331 | 1332 | $result = deactivate_plugins( $deactivate ); 1333 | 1334 | if ( ! is_wp_error( $result ) ) { 1335 | wp_redirect( $redirect ); 1336 | } 1337 | } 1338 | 1339 | if ( ! $error && is_wp_error( $result ) ) { 1340 | $error = $result->get_error_message(); 1341 | } 1342 | } 1343 | 1344 | /* 1345 | * If an error was found at any point, we can queue it to 1346 | * display. 1347 | * 1348 | * Otherwise, if an activation or deactivation were successful, 1349 | * the user would have already been redirected by this point. 1350 | */ 1351 | if ( $error ) { 1352 | 1353 | add_settings_error( 1354 | 'plugin-manager', 1355 | 'plugin-manager-error', 1356 | $error, 1357 | 'error' 1358 | ); 1359 | 1360 | } 1361 | 1362 | } 1363 | } 1364 | } 1365 | -------------------------------------------------------------------------------- /src/class-my-plugin-notices.php: -------------------------------------------------------------------------------- 1 | args = wp_parse_args( $args, array( 81 | 'package_name' => '', 82 | 'package_url' => '', 83 | 'admin_url' => '', 84 | 'nag_action' => '', 85 | 'nag_dismiss' => '', 86 | 'nag_update' => '', 87 | 'nag_install_single' => '', 88 | 'nag_install_multiple' => '', 89 | )); 90 | 91 | $this->manager = $manager; 92 | 93 | $this->plugins = $plugins; 94 | 95 | // Determine admin-wide notices. 96 | add_action( 'admin_enqueue_scripts', array( $this, 'set_notices' ) ); 97 | 98 | // Print necessary scripts for admin-wide notices. 99 | add_action( 'admin_enqueue_scripts', array( $this, 'add_assets' ) ); 100 | 101 | // Display admin-wide notices. 102 | add_action( 'admin_notices', array( $this, 'add_notices' ) ); 103 | 104 | // Handle notice dismissals through Ajax. 105 | add_action( 'wp_ajax_my_namespace-dismiss-notice', array( $this, 'dismiss' ) ); 106 | 107 | } 108 | 109 | /** 110 | * Sets admin plugin notices, which are seen 111 | * throughout the entire admin. 112 | * 113 | * @since 1.0.0 114 | */ 115 | public function set_notices() { 116 | 117 | // DEBUG: 118 | // delete_user_meta( get_current_user_id(), 'my_namespace-dismiss-plugin-notice_update' ); 119 | // delete_user_meta( get_current_user_id(), 'my_namespace-dismiss-plugin-notice_install' ); 120 | 121 | $this->notices = array(); 122 | 123 | /* 124 | * Because our notices are intended to lead the user to 125 | * our admin screen, the notices should show everywhere 126 | * except our admin screen. 127 | */ 128 | if ( $this->manager->is_admin_screen() ) { 129 | return; 130 | } 131 | 132 | $plugins = $this->plugins->get(); 133 | 134 | /* 135 | * If necessary, add notice to install plugins. 136 | * 137 | * If the message hasn't been dismissed, this will tell the 138 | * user how many suggested plugins have the `not-installed` 139 | * status. 140 | */ 141 | $to_install = array(); 142 | 143 | foreach ( $plugins as $plugin ) { 144 | if ( 'not-installed' === $plugin['status'] ) { 145 | $to_install[] = $plugin['slug']; 146 | } 147 | } 148 | 149 | if ( $to_install ) { 150 | 151 | /* 152 | * For the install notice, we want to use all suggested 153 | * plugins, and not just the count of plugins still to 154 | * be installed. 155 | * 156 | * This is so if the user dimisses the notice, they won't 157 | * see it again by installing and uninstalling plugins. 158 | * However, they will see it if the theme author adds 159 | * more suggested plugins. 160 | */ 161 | $check = implode( ',', array_keys( $plugins ) ); // All suggested plugins, not just those in $to_install. 162 | 163 | if ( get_user_meta( get_current_user_id(), 'my_namespace-dismiss-plugin-notice_install', true ) !== $check ) { 164 | 165 | $count = count( $to_install ); 166 | 167 | if ( 0 === $count || $count >= 2 ) { 168 | $plural = 'multiple'; 169 | } else { 170 | $plural = 'single'; 171 | } 172 | 173 | $this->notices['install']['message'] = sprintf( 174 | $this->args[ 'nag_install_' . $plural ], 175 | $this->args['package_name'], 176 | $count 177 | ); 178 | 179 | $this->notices['install']['value'] = $check; 180 | 181 | } 182 | } 183 | 184 | /* 185 | * If necessary, add notice to update plugins. 186 | * 187 | * If the message hasn't been dismissed, this will tell the 188 | * user how many plugins have status `incompatible`. 189 | * 190 | * For a plugin to be marked incompatible, it must be active 191 | * and it's suggested version must be higher than its 192 | * installed version. 193 | */ 194 | $to_update = array(); 195 | 196 | foreach ( $plugins as $plugin ) { 197 | if ( 'incompatible' === $plugin['status'] ) { 198 | $to_update[] = $plugin['slug']; 199 | } 200 | } 201 | 202 | if ( $to_update ) { 203 | 204 | /* 205 | * For the update notice, we want the notice to appear 206 | * any time the string of incompatible plugins changes. 207 | */ 208 | $check = implode( ',', $to_update ); 209 | 210 | if ( get_user_meta( get_current_user_id(), 'my_namespace-dismiss-plugin-notice_update', true ) !== $check ) { 211 | 212 | $this->notices['update']['message'] = sprintf( 213 | $this->args['nag_update'], 214 | $this->args['package_name'] 215 | ); 216 | 217 | $this->notices['update']['value'] = $check; 218 | 219 | } 220 | } 221 | 222 | } 223 | 224 | /** 225 | * Add any assets needed for plugin notices. 226 | * 227 | * @since 1.0.0 228 | */ 229 | public function add_assets() { 230 | 231 | if ( ! $this->notices ) { 232 | return; 233 | } 234 | 235 | $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; 236 | 237 | wp_enqueue_script( 238 | 'my_namespace-plugin-notices', 239 | esc_url( $this->args['package_url'] . "/assets/js/plugin-notices$suffix.js" ), 240 | array( 'jquery' ) 241 | ); 242 | 243 | } 244 | 245 | /** 246 | * Handles admin plugin notices, which are seen 247 | * throughout the entire admin. 248 | * 249 | * @since 1.0.0 250 | */ 251 | public function add_notices() { 252 | 253 | if ( $this->notices ) { 254 | 255 | foreach ( $this->notices as $key => $data ) { 256 | 257 | $this->display( $key, $data['value'], $data['message'] ); 258 | 259 | } 260 | } 261 | 262 | } 263 | 264 | /** 265 | * Display custom admin notice. 266 | * 267 | * @since 1.0.0 268 | */ 269 | public function display( $key, $value, $message ) { 270 | 271 | $class = 'plugin-manager-notice notice is-dismissible'; 272 | 273 | if ( 'update' === $key ) { 274 | $class .= ' notice-warning'; 275 | } else { 276 | $class .= ' notice-info'; 277 | } 278 | 279 | $nonce = wp_create_nonce( 'plugin-manager-dismiss-notice_' . $key ); 280 | 281 | $action_link = sprintf( 282 | '%2$s', 283 | esc_url( $this->args['admin_url'] ), 284 | esc_html( $this->args['nag_action'] ) 285 | ); 286 | 287 | $dismiss_link = sprintf( 288 | '%2$s', 289 | esc_url( $this->args['admin_url'] ), 290 | esc_html( $this->args['nag_dismiss'] ) 291 | ); 292 | 293 | ?> 294 |
295 | 296 |

297 | 298 |

299 | 300 |
301 | pre_plugins = $pre_plugins; 58 | 59 | $this->manager = $manager; 60 | 61 | /* 62 | * Setup plugins. The formatting for this must get 63 | * hooked later in the WordPress loading process to 64 | * make use of /wp-admin/includes/plugin.php. 65 | */ 66 | add_action( 'current_screen', array( $this, 'set' ) ); 67 | 68 | } 69 | 70 | /** 71 | * Consistently format plugins and add some 72 | * helpful info. 73 | * 74 | * @since 1.0.0 75 | */ 76 | public function set() { 77 | 78 | /* 79 | * If we're loading our admin screen, we can refresh 80 | * the WordPress plugin update cache to show the 81 | * latest results. 82 | */ 83 | if ( $this->manager->is_admin_screen() ) { 84 | wp_update_plugins(); 85 | } 86 | 87 | /* 88 | * Get all currently installed plugins, that exist 89 | * from suggested plugin list, including which have 90 | * updates ready to install. 91 | */ 92 | $installed = $this->get_installed(); 93 | 94 | /* 95 | * Loop through passed in plugins, looking for matches 96 | * with installed plugins, and store results to $plugins. 97 | */ 98 | foreach ( $this->pre_plugins as $plugin ) { 99 | 100 | $info = array(); 101 | 102 | if ( array_key_exists( $plugin['slug'], $installed ) ) { 103 | $info = $installed[ $plugin['slug'] ]; 104 | } 105 | 106 | $this->add( $plugin, $info ); 107 | 108 | } 109 | 110 | // Alphabatize plugins by slug. 111 | ksort( $this->plugins ); 112 | 113 | } 114 | 115 | /** 116 | * Check if plugins have been setup and formatted. 117 | * 118 | * @since 1.0.0 119 | * 120 | * @return bool If $plugins has been properly setup. 121 | */ 122 | public function is_set() { 123 | 124 | if ( $this->plugins ) { 125 | return true; 126 | } 127 | 128 | return false; 129 | 130 | } 131 | 132 | /** 133 | * Format and store plugin to $this->plugins. 134 | * 135 | * The "status" of a plugin can be one of the following: 136 | * 137 | * 1. `not-installed` It's simply not installed. 138 | * 2. `inactive` Installed, but not activated yet. 139 | * 3. `incompatible` Activated, but installed version is less than suggested version. 140 | * 4. `active` Installed, activated and compatible. 141 | * 142 | * Note: The `incompatible` status will only be applied 143 | * if the current version is less than the suggsted version; 144 | * it will NOT be applied simply because WordPress says 145 | * the plugin has an update available. 146 | * 147 | * The final stored plugin data will be an array formatted, 148 | * as follows. 149 | * 150 | * $plugin { 151 | * @type string $name Name of plugin, like `My Plugin`. 152 | * @type string $slug Slug of plugin, like `my-plugin`. 153 | * @type string $url URL to plugin website, ONLY if not on wordpress.org. 154 | * @type string $version Suggested plugin version, like `2.0+`. 155 | * @type string $current_version Current installed version of plugin. 156 | * @type string $new_version Latest available version of plugin. 157 | * @type string $file Plugin file, like `my-plugin/my-plugin.php`. 158 | * @type string $status Plugin status key (see above). 159 | * @type bool $update Whether WordPress says the plugin has an update available. 160 | * @type array $notice { 161 | * Optional. Notice for plugin, if needed. 162 | * 163 | * @type string $message Message for notice. 164 | * @type string $class CSS class(es) for notice. 165 | * } 166 | * } 167 | * 168 | * @since 1.0.0 169 | * 170 | * @param array $plugin { 171 | * Plugin info from initial object creation. 172 | * 173 | * @type string $name Name of plugin, like `My Plugin`. 174 | * @type string $slug Slug of plugin, like `my-plugin`. 175 | * @type string $url URL to plugin website, ONLY if not on wordpress.org. 176 | * @type string $version Suggested plugin version, like `2.0+`. 177 | * } 178 | * @param array $info { 179 | * Optional. Plugin info from WP, if installed. 180 | * 181 | * @type string $file Location of plugin file. 182 | * @type string $current_version Current installed version. 183 | * @type string $new_version Newest version available. 184 | * @type bool $is_active Whether plugin is active. 185 | * } 186 | */ 187 | public function add( $plugin, $info = null ) { 188 | 189 | // Merge initial plugin data with data abstract. 190 | $plugin = array_merge( array( 191 | 'name' => null, 192 | 'slug' => null, 193 | 'url' => null, 194 | 'version' => null, 195 | 'current_version' => null, 196 | 'new_version' => null, 197 | 'file' => null, 198 | 'status' => 'not-installed', 199 | 'update' => false, 200 | 'notice' => '', 201 | ), $plugin ); 202 | 203 | /* 204 | * If installed data is null, it means it hasn't been 205 | * attempted to be retrieved. If it has been retrieved 206 | * but was empty (i.e plugin isn't' installed), then 207 | * an empty array should be passed for $info. 208 | */ 209 | if ( null === $info ) { 210 | 211 | $installed = $this->get_installed(); 212 | 213 | if ( array_key_exists( $plugin['slug'], $installed ) ) { 214 | $info = $installed[ $plugin['slug'] ]; 215 | } 216 | } 217 | 218 | /* 219 | * At this point, if $info is empty the plugin status 220 | * shall remain as 'not-installed' and the following 221 | * setup will be skipped. 222 | */ 223 | if ( $info ) { 224 | 225 | // Add plugin file location. 226 | if ( ! empty( $info['file'] ) ) { 227 | $plugin['file'] = $info['file']; 228 | } 229 | 230 | // Add currently installed version. 231 | if ( ! empty( $info['current_version'] ) ) { 232 | $plugin['current_version'] = $info['current_version']; 233 | } 234 | 235 | // Add latest available version to check against current. 236 | if ( ! empty( $info['new_version'] ) ) { 237 | $plugin['new_version'] = $info['new_version']; 238 | } 239 | 240 | // Set plugin status. 241 | if ( ! empty( $info['is_active'] ) ) { 242 | 243 | if ( version_compare( $plugin['version'], $plugin['current_version'], '>' ) ) { 244 | $plugin['status'] = 'incompatible'; 245 | } else { 246 | $plugin['status'] = 'active'; 247 | } 248 | } else { 249 | 250 | $plugin['status'] = 'inactive'; 251 | 252 | } 253 | 254 | // Add plugin notice for available update. 255 | if ( version_compare( $plugin['new_version'], $plugin['current_version'], '>' ) ) { 256 | 257 | $plugin['update'] = true; 258 | 259 | } 260 | } 261 | 262 | if ( ! $plugin['url'] ) { 263 | 264 | $plugin['url'] = 'https://wordpress.org/plugins/' . $plugin['slug']; 265 | 266 | } 267 | 268 | // Store final, formatted plugin. 269 | $this->plugins[ $plugin['slug'] ] = $plugin; 270 | 271 | } 272 | 273 | /** 274 | * Get data for a plugin, or data for all plugins. 275 | * 276 | * @since 1.0.0 277 | * 278 | * @param string $slug Optional. Slug of plugin to retrieve data for. Leave empty for all plugins. 279 | * @return array|bool Data for all plugins, data for single plugin, or `false` if single plugin doesn't exist. 280 | */ 281 | public function get( $slug = '' ) { 282 | 283 | if ( ! $slug ) { 284 | return $this->plugins; 285 | } 286 | 287 | if ( isset( $this->plugins[ $slug ] ) ) { 288 | return $this->plugins[ $slug ]; 289 | } 290 | 291 | return false; 292 | 293 | } 294 | 295 | /** 296 | * Match installed plugins from WordPress against 297 | * our suggested plugins, to return information 298 | * for our suggested plugins, wnich are currently 299 | * installed. 300 | * 301 | * @since 1.0.0 302 | * 303 | * @return array $installed Installed plugin that are suggested. 304 | */ 305 | public function get_installed() { 306 | 307 | // Create array of our suggested plugin slugs. 308 | $slugs = array(); 309 | 310 | foreach ( $this->pre_plugins as $plugin ) { 311 | $slugs[] = $plugin['slug']; 312 | } 313 | 314 | // Gather installed plugins from our suggested plugins. 315 | $installed = array(); 316 | 317 | $plugins = get_plugins(); 318 | 319 | foreach ( $plugins as $key => $plugin ) { 320 | 321 | $data = get_plugin_data( WP_PLUGIN_DIR . '/' . $key ); 322 | 323 | $slug = dirname( plugin_basename( $key ) ); 324 | 325 | if ( ! in_array( $slug, $slugs ) ) { 326 | continue; 327 | } 328 | 329 | $installed[ $slug ] = array( 330 | 'slug' => $slug, 331 | 'file' => $key, 332 | 'current_version' => $data['Version'], 333 | 'new_version' => $data['Version'], 334 | 'is_active' => is_plugin_active( $key ), 335 | ); 336 | 337 | } 338 | 339 | // Determine which of those installed plugins have an update. 340 | $plugin_updates = get_site_transient( 'update_plugins' ); 341 | 342 | if ( isset( $plugin_updates->response ) ) { 343 | 344 | foreach ( $plugin_updates->response as $plugin ) { 345 | 346 | if ( ! in_array( $plugin->slug, $slugs ) ) { 347 | continue; 348 | } 349 | 350 | $installed[ $plugin->slug ]['new_version'] = $plugin->new_version; 351 | 352 | } 353 | } 354 | 355 | return $installed; 356 | 357 | } 358 | 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /src/example-child-theme.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * Setup suggested plugin system. 3 | * 4 | * Include the {{My}}_Plugin_Manager class and add 5 | * an interface for users to to manage suggested 6 | * plugins. 7 | * 8 | * @since x.x.x 9 | * 10 | * @see {{My}}_Plugin_Manager 11 | * @link http://mypluginmanager.com 12 | */ 13 | function {{my}}_plugin_manager() { 14 | 15 | if ( ! is_admin() ) { 16 | return; 17 | } 18 | 19 | /** 20 | * Include plugin manager class. 21 | * 22 | * No other includes are needed. The {{My}}_Plugin_Manager 23 | * class will handle including any other files needed. 24 | * 25 | * If you want to move the "plugin-manager" directory to 26 | * a different location within your child theme, that's 27 | * totally fine; just make sure you adjust this include 28 | * path to the plugin manager class accordingly. 29 | */ 30 | require_once( get_theme_file_path( '/plugin-manager/{{class-my-}}plugin-manager.php' ) ); 31 | 32 | // Setup suggested plugins. 33 | $plugins = array( 34 | array( 35 | 'name' => 'Easy Digital Downloads', 36 | 'slug' => 'easy-digital-downloads', 37 | 'version' => '2.8+', 38 | ), 39 | array( 40 | 'name' => 'JetPack', 41 | 'slug' => 'jetpack', 42 | 'version' => '5.0+', 43 | ), 44 | array( 45 | 'name' => 'Gravity Forms', 46 | 'slug' => 'gravityforms', 47 | 'version' => '2.2+', 48 | 'url' => 'http://www.gravityforms.com', // Only for non wp.org plugins. 49 | ), 50 | ); 51 | 52 | /* 53 | * Setup optional arguments for plugin manager interface. 54 | * 55 | * See the set_args() method of the {{My}}_Plugin_Manager 56 | * class for full documentation on what you can pass in here. 57 | */ 58 | $args = array( 59 | 'page_title' => __( 'Suggested Plugins', '{{text-domain}}' ), 60 | 'menu_slug' => '{{text-domain}}-suggested-plugins', 61 | 'child_theme' => true, 62 | ); 63 | 64 | /* 65 | * Create plugin manager object, passing in the suggested 66 | * plugins and optional arguments. 67 | */ 68 | $manager = new {{My}}_Plugin_Manager( $plugins, $args ); 69 | 70 | } 71 | add_action( 'after_setup_theme', '{{my}}_plugin_manager' ); 72 | -------------------------------------------------------------------------------- /src/example-plugin.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * Setup suggested plugin system. 3 | * 4 | * Include the {{My}}_Plugin_Manager class and add 5 | * an interface for users to to manage suggested 6 | * plugins. 7 | * 8 | * @since x.x.x 9 | * 10 | * @see {{My}}_Plugin_Manager 11 | * @link http://mypluginmanager.com 12 | */ 13 | function {{my}}_plugin_manager() { 14 | 15 | if ( ! is_admin() ) { 16 | return; 17 | } 18 | 19 | /** 20 | * Include plugin manager class. 21 | * 22 | * No other includes are needed. The {{My}}_Plugin_Manager 23 | * class will handle including any other files needed. 24 | * 25 | * If you want to move the "plugin-manager" directory to 26 | * a different location within your plugin, that's totally 27 | * fine; just make sure you adjust this include path to 28 | * the plugin manager class accordingly. 29 | */ 30 | require_once( plugin_dir_path( __FILE__ ) . '/plugin-manager/{{class-my-}}plugin-manager.php' ); 31 | 32 | /* 33 | * Setup suggested plugins. 34 | * 35 | * It's a good idea to have a filter applied to this so your 36 | * loyal dev users have an easy way to modify what plugins 37 | * are suggested. 38 | */ 39 | $plugins = apply_filters( '{{my}}_plugins', array( 40 | array( 41 | 'name' => 'Easy Digital Downloads', 42 | 'slug' => 'easy-digital-downloads', 43 | 'version' => '2.8+', 44 | ), 45 | array( 46 | 'name' => 'JetPack', 47 | 'slug' => 'jetpack', 48 | 'version' => '5.0+', 49 | ), 50 | array( 51 | 'name' => 'Gravity Forms', 52 | 'slug' => 'gravityforms', 53 | 'version' => '2.2+', 54 | 'url' => 'http://www.gravityforms.com', // Only for non wp.org plugins. 55 | ), 56 | )); 57 | 58 | /* 59 | * Setup optional arguments for plugin manager interface. 60 | * 61 | * See the set_args() method of the {{My}}_Plugin_Manager 62 | * class for full documentation on what you can pass in here. 63 | */ 64 | $args = array( 65 | 'page_title' => __( 'Suggested by {{name}}', '{{text-domain}}' ), 66 | 'menu_title' => __( 'Suggested', '{{text-domain}}' ), 67 | 'menu_slug' => '{{text-domain}}-suggested-plugins', 68 | 'parent_slug' => 'plugins.php', 69 | 'plugin_file' => __FILE__, // Required for use in plugins. 70 | ); 71 | 72 | /* 73 | * Create plugin manager object, passing in the suggested 74 | * plugins and optional arguments. 75 | */ 76 | $manager = new {{My}}_Plugin_Manager( $plugins, $args ); 77 | 78 | } 79 | add_action( 'after_setup_theme', '{{my}}_plugin_manager' ); 80 | -------------------------------------------------------------------------------- /src/example-theme.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * Setup suggested plugin system. 3 | * 4 | * Include the {{My}}_Plugin_Manager class and add 5 | * an interface for users to to manage suggested 6 | * plugins. 7 | * 8 | * @since x.x.x 9 | * 10 | * @see {{My}}_Plugin_Manager 11 | * @link http://mypluginmanager.com 12 | */ 13 | function {{my}}_plugin_manager() { 14 | 15 | if ( ! is_admin() ) { 16 | return; 17 | } 18 | 19 | /** 20 | * Include plugin manager class. 21 | * 22 | * No other includes are needed. The {{My}}_Plugin_Manager 23 | * class will handle including any other files needed. 24 | * 25 | * If you want to move the "plugin-manager" directory to 26 | * a different location within your theme, that's totally 27 | * fine; just make sure you adjust this include path to 28 | * the plugin manager class accordingly. 29 | */ 30 | require_once( get_parent_theme_file_path( '/plugin-manager/{{class-my-}}plugin-manager.php' ) ); 31 | 32 | /* 33 | * Setup suggested plugins. 34 | * 35 | * It's a good idea to have a filter applied to this so your 36 | * loyal users running child themes have a way to easily modify 37 | * which plugins show as suggested for the site they're setting 38 | * up for a client. 39 | */ 40 | $plugins = apply_filters( '{{my}}_plugins', array( 41 | array( 42 | 'name' => 'Easy Digital Downloads', 43 | 'slug' => 'easy-digital-downloads', 44 | 'version' => '2.8+', 45 | ), 46 | array( 47 | 'name' => 'JetPack', 48 | 'slug' => 'jetpack', 49 | 'version' => '5.0+', 50 | ), 51 | array( 52 | 'name' => 'Gravity Forms', 53 | 'slug' => 'gravityforms', 54 | 'version' => '2.2+', 55 | 'url' => 'http://www.gravityforms.com', // Only for non wp.org plugins. 56 | ), 57 | )); 58 | 59 | /* 60 | * Setup optional arguments for plugin manager interface. 61 | * 62 | * See the set_args() method of the {{My}}_Plugin_Manager 63 | * class for full documentation on what you can pass in here. 64 | */ 65 | $args = array( 66 | 'page_title' => __( 'Suggested Plugins', '{{text-domain}}' ), 67 | 'menu_slug' => '{{text-domain}}-suggested-plugins', 68 | ); 69 | 70 | /* 71 | * Create plugin manager object, passing in the suggested 72 | * plugins and optional arguments. 73 | */ 74 | $manager = new {{My}}_Plugin_Manager( $plugins, $args ); 75 | 76 | } 77 | add_action( 'after_setup_theme', '{{my}}_plugin_manager' ); 78 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const addUnminified = require('unminified-webpack-plugin'); 4 | 5 | module.exports = ({ entry, script }) => { 6 | return { 7 | entry, 8 | output: { 9 | // Note: The typical "path" is replaced with gulp.dest in 10 | // task, and so only filename is needed here. 11 | filename: `./plugin-manager/assets/js/plugin-${script}.min.js` 12 | }, 13 | mode: 'production', 14 | externals: { 15 | wp: 'wp', 16 | jquery: 'jQuery', 17 | settings: 'pluginManagerSettings' 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.js$/, 23 | exclude: /node_modules/, 24 | use: ['babel-loader'] 25 | } 26 | ] 27 | }, 28 | plugins: [new addUnminified()] 29 | }; 30 | }; 31 | --------------------------------------------------------------------------------