editor-view.js
9 months ago
gutenberg-video-upload.js
6 months ago
media-video-widget-extensions.js
6 years ago
videopress-add-resumable-upload-support.js
4 years ago
videopress-media-new.js
1 month ago
videopress-plupload.js
8 months ago
videopress-uploader.js
2 months ago
videopress-media-new.js
345 lines
| 1 | /* global plupload, pluploadL10n, ajaxurl, wpUploaderInit, uploadSuccess, wpFileError, wpQueueError, videoPressMediaNew */ |
| 2 | |
| 3 | /** |
| 4 | * Routes video uploads from wp-admin/media-new.php to VideoPress. |
| 5 | * |
| 6 | * The classic uploader (plupload-handlers) creates a raw plupload.Uploader |
| 7 | * rather than wp.Uploader, so the override in videopress-plupload.js never |
| 8 | * engages there. This shim registers the same videopress_check_uploads file |
| 9 | * filter and re-targets video uploads to VideoPress, then hands the local |
| 10 | * attachment ID created during the upload back to the stock uploadSuccess |
| 11 | * handler so the classic UI renders the media item as usual. |
| 12 | */ |
| 13 | ( function ( $ ) { |
| 14 | if ( typeof plupload === 'undefined' ) { |
| 15 | return; |
| 16 | } |
| 17 | |
| 18 | var originalOptions = {}; |
| 19 | |
| 20 | // Videos accepted for VideoPress upload since the page loaded, used to |
| 21 | // enforce the free plan's single-video limit within an upload session. |
| 22 | var acceptedVideoCount = 0; |
| 23 | |
| 24 | // Video files in the selection currently being added to the uploader, |
| 25 | // recorded before the file filters run so the free-plan gate can block a |
| 26 | // multi-video selection before any upload starts. |
| 27 | var pendingBatchVideoCount = 0; |
| 28 | |
| 29 | /** |
| 30 | * Counts the video files in an addFile() argument. |
| 31 | * |
| 32 | * @param {Array|FileList|object} file What was passed to addFile: a single |
| 33 | * file or an array-like of files. |
| 34 | * @return {number} How many of them are videos. |
| 35 | */ |
| 36 | function countVideoFiles( file ) { |
| 37 | var files = file && typeof file.length === 'number' ? file : [ file ]; |
| 38 | var count = 0; |
| 39 | |
| 40 | for ( var i = 0; i < files.length; i++ ) { |
| 41 | var type = files[ i ] && files[ i ].type; |
| 42 | |
| 43 | if ( type && type.split( '/' )[ 0 ] === 'video' ) { |
| 44 | count++; |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | return count; |
| 49 | } |
| 50 | |
| 51 | /** |
| 52 | * Returns the upload limit data localized by the module. |
| 53 | * |
| 54 | * @return {object} The limit data. |
| 55 | */ |
| 56 | function getUploadLimits() { |
| 57 | return typeof videoPressMediaNew !== 'undefined' ? videoPressMediaNew : {}; |
| 58 | } |
| 59 | |
| 60 | // Class of the container the upgrade notice renders in. |
| 61 | var NOTICE_CLASS = 'videopress-media-new-notice'; |
| 62 | |
| 63 | /** |
| 64 | * Removes any previously rendered upgrade notice. |
| 65 | */ |
| 66 | function removeUpgradeNotice() { |
| 67 | var notices = document.querySelectorAll( '.' + NOTICE_CLASS ); |
| 68 | |
| 69 | for ( var i = 0; i < notices.length; i++ ) { |
| 70 | notices[ i ].parentNode.removeChild( notices[ i ] ); |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | /** |
| 75 | * Shows the free-plan upgrade notice above the upload queue. |
| 76 | * |
| 77 | * The message is pre-escaped HTML built server-side, containing the link |
| 78 | * to the upgrade path. It renders in a container of its own next to the |
| 79 | * stock #media-upload-error area rather than inside it: the stock |
| 80 | * FilesAdded handler empties #media-upload-error as soon as any file of |
| 81 | * the selection is accepted, which would wipe a notice rendered there |
| 82 | * before the user could read it. |
| 83 | * |
| 84 | * @param {string} message The notice HTML. |
| 85 | */ |
| 86 | function showUpgradeNotice( message ) { |
| 87 | if ( ! message ) { |
| 88 | return; |
| 89 | } |
| 90 | |
| 91 | var anchor = document.getElementById( 'media-upload-error' ); |
| 92 | |
| 93 | if ( ! anchor || ! anchor.parentNode ) { |
| 94 | // The stock error area is better than no notice at all. |
| 95 | if ( typeof wpQueueError === 'function' ) { |
| 96 | wpQueueError( message ); |
| 97 | } |
| 98 | return; |
| 99 | } |
| 100 | |
| 101 | removeUpgradeNotice(); |
| 102 | |
| 103 | var notice = document.createElement( 'div' ); |
| 104 | notice.className = 'notice notice-warning ' + NOTICE_CLASS; |
| 105 | |
| 106 | var paragraph = document.createElement( 'p' ); |
| 107 | // The message is localized server-side and passed through wp_kses; |
| 108 | // it only contains the upgrade link markup. |
| 109 | paragraph.innerHTML = message; |
| 110 | |
| 111 | notice.appendChild( paragraph ); |
| 112 | anchor.parentNode.insertBefore( notice, anchor.nextSibling ); |
| 113 | } |
| 114 | |
| 115 | /** |
| 116 | * Free-plan gate for a video file: the free plan includes a single video |
| 117 | * upload, so reject every video once the site already has a VideoPress |
| 118 | * video, and allow only one video per upload session otherwise. |
| 119 | * |
| 120 | * @param {plupload.File} file The video file being queued. |
| 121 | * @param {Function} cb The file filter continuation callback. |
| 122 | * @return {boolean} Whether the file was rejected. |
| 123 | */ |
| 124 | function rejectedByFreePlanLimits( file, cb ) { |
| 125 | var limits = getUploadLimits(); |
| 126 | var strings = limits.strings || {}; |
| 127 | |
| 128 | if ( limits.hasVideoPressPurchase ) { |
| 129 | return false; |
| 130 | } |
| 131 | |
| 132 | if ( limits.hasUsedVideo ) { |
| 133 | showUpgradeNotice( strings.usedVideoUpload ); |
| 134 | cb( false ); |
| 135 | return true; |
| 136 | } |
| 137 | |
| 138 | // A selection with more than one video is blocked entirely — including |
| 139 | // its first video — so no upload starts until the user upgrades. |
| 140 | if ( pendingBatchVideoCount > 1 ) { |
| 141 | showUpgradeNotice( strings.multipleVideosSelected ); |
| 142 | cb( false ); |
| 143 | return true; |
| 144 | } |
| 145 | |
| 146 | if ( acceptedVideoCount >= 1 ) { |
| 147 | showUpgradeNotice( strings.multipleVideos ); |
| 148 | cb( false ); |
| 149 | return true; |
| 150 | } |
| 151 | |
| 152 | return false; |
| 153 | } |
| 154 | |
| 155 | /** |
| 156 | * Restores the stock upload settings saved before a VideoPress upload. |
| 157 | * |
| 158 | * @param {plupload.Uploader} up Uploader instance. |
| 159 | */ |
| 160 | function restoreOriginalOptions( up ) { |
| 161 | if ( typeof originalOptions.url === 'undefined' ) { |
| 162 | return; |
| 163 | } |
| 164 | |
| 165 | up.setOption( 'url', originalOptions.url ); |
| 166 | up.setOption( 'file_data_name', originalOptions.file_data_name ); |
| 167 | up.setOption( 'headers', originalOptions.headers ); |
| 168 | |
| 169 | originalOptions = {}; |
| 170 | } |
| 171 | |
| 172 | /** |
| 173 | * For video files, fetch a VideoPress upload token and attach it to the |
| 174 | * file before it is queued. Mirrors the videopress_check_uploads filter in |
| 175 | * videopress-uploader.js without depending on wp.media, which is not |
| 176 | * loaded on media-new.php. Non-video files get the stock max_file_size |
| 177 | * behavior this filter replaces. |
| 178 | */ |
| 179 | plupload.addFileFilter( 'videopress_check_uploads', function ( maxSize, file, cb ) { |
| 180 | var self = this; |
| 181 | |
| 182 | /** |
| 183 | * Rejects the file with an uploader error. |
| 184 | * |
| 185 | * @param {string} [message] Error message to display. |
| 186 | */ |
| 187 | function fail( message ) { |
| 188 | self.trigger( 'Error', { |
| 189 | code: plupload.HTTP_ERROR, |
| 190 | message: |
| 191 | message || |
| 192 | plupload.translate( 'Could not get the VideoPress token needed for uploading' ), |
| 193 | file: file, |
| 194 | } ); |
| 195 | cb( false ); |
| 196 | } |
| 197 | |
| 198 | if ( file.type && file.type.split( '/' )[ 0 ] === 'video' ) { |
| 199 | if ( rejectedByFreePlanLimits( file, cb ) ) { |
| 200 | return; |
| 201 | } |
| 202 | |
| 203 | $.post( ajaxurl, { action: 'videopress-get-upload-token', filename: file.name } ) |
| 204 | .done( function ( response ) { |
| 205 | if ( |
| 206 | ! response || |
| 207 | ! response.success || |
| 208 | ! response.data || |
| 209 | ! response.data.upload_action_url |
| 210 | ) { |
| 211 | fail( response && response.data && response.data.message ); |
| 212 | return; |
| 213 | } |
| 214 | |
| 215 | file.videopress = response.data; |
| 216 | acceptedVideoCount++; |
| 217 | cb( true ); |
| 218 | } ) |
| 219 | .fail( function () { |
| 220 | fail(); |
| 221 | } ); |
| 222 | |
| 223 | return; |
| 224 | } |
| 225 | |
| 226 | // Handles the stock max_file_size functionality for non-video files. |
| 227 | if ( |
| 228 | typeof file.size !== 'undefined' && |
| 229 | maxSize && |
| 230 | file.size > plupload.parseSize( maxSize ) |
| 231 | ) { |
| 232 | this.trigger( 'Error', { |
| 233 | code: plupload.FILE_SIZE_ERROR, |
| 234 | message: pluploadL10n.file_exceeds_size_limit.replace( '%s', file.name ), |
| 235 | file: file, |
| 236 | } ); |
| 237 | cb( false ); |
| 238 | return; |
| 239 | } |
| 240 | |
| 241 | cb( true ); |
| 242 | } ); |
| 243 | |
| 244 | $( function () { |
| 245 | // plupload-handlers is a dependency of this script, so its ready |
| 246 | // callback ran first and the global `uploader` already exists. |
| 247 | if ( |
| 248 | typeof wpUploaderInit === 'undefined' || |
| 249 | typeof window.uploader === 'undefined' || |
| 250 | ! window.uploader |
| 251 | ) { |
| 252 | return; |
| 253 | } |
| 254 | |
| 255 | var uploader = window.uploader; |
| 256 | |
| 257 | /** |
| 258 | * Record how many videos each selection contains before plupload runs |
| 259 | * the file filters, so the free-plan gate can reject every video of a |
| 260 | * multi-video selection. Both the file selector and drag-and-drop |
| 261 | * funnel their whole selection through a single addFile() call. |
| 262 | */ |
| 263 | var stockAddFile = uploader.addFile; |
| 264 | |
| 265 | uploader.addFile = function ( file, fileName ) { |
| 266 | // Each selection starts clean: a notice about a previous |
| 267 | // selection no longer applies to this one. |
| 268 | removeUpgradeNotice(); |
| 269 | pendingBatchVideoCount = countVideoFiles( file ); |
| 270 | return stockAddFile.call( this, file, fileName ); |
| 271 | }; |
| 272 | |
| 273 | /** |
| 274 | * Re-target VideoPress files right before each upload starts and |
| 275 | * restore the stock settings for everything else, so mixed batches |
| 276 | * of videos and other media both land in the right place. |
| 277 | */ |
| 278 | uploader.bind( 'BeforeUpload', function ( up, file ) { |
| 279 | if ( typeof file.videopress === 'undefined' ) { |
| 280 | restoreOriginalOptions( up ); |
| 281 | return; |
| 282 | } |
| 283 | |
| 284 | if ( typeof originalOptions.url === 'undefined' ) { |
| 285 | originalOptions.url = up.getOption( 'url' ); |
| 286 | originalOptions.file_data_name = up.getOption( 'file_data_name' ); |
| 287 | originalOptions.headers = up.getOption( 'headers' ); |
| 288 | } |
| 289 | |
| 290 | up.setOption( 'url', file.videopress.upload_action_url ); |
| 291 | up.setOption( 'file_data_name', 'media[]' ); |
| 292 | up.setOption( 'headers', { |
| 293 | Authorization: |
| 294 | 'X_UPLOAD_TOKEN token="' + |
| 295 | file.videopress.upload_token + |
| 296 | '" blog_id="' + |
| 297 | file.videopress.upload_blog_id + |
| 298 | '"', |
| 299 | } ); |
| 300 | } ); |
| 301 | |
| 302 | uploader.bind( 'Error', function ( up ) { |
| 303 | restoreOriginalOptions( up ); |
| 304 | } ); |
| 305 | |
| 306 | uploader.bind( 'UploadComplete', function ( up ) { |
| 307 | restoreOriginalOptions( up ); |
| 308 | } ); |
| 309 | |
| 310 | /** |
| 311 | * The stock FileUploaded handler calls the global uploadSuccess with |
| 312 | * the raw server response, which for VideoPress uploads is the REST |
| 313 | * API media response instead of the attachment ID async-upload.php |
| 314 | * returns. Wrap uploadSuccess to translate the former into the |
| 315 | * latter; the stock handler then loads the media item row from |
| 316 | * async-upload.php as it would for any other upload. |
| 317 | */ |
| 318 | var stockUploadSuccess = uploadSuccess; |
| 319 | |
| 320 | window.uploadSuccess = function ( fileObj, serverData ) { |
| 321 | if ( typeof fileObj.videopress === 'undefined' ) { |
| 322 | return stockUploadSuccess( fileObj, serverData ); |
| 323 | } |
| 324 | |
| 325 | var response = null; |
| 326 | |
| 327 | try { |
| 328 | response = JSON.parse( serverData ); |
| 329 | } catch { |
| 330 | // Handled below. |
| 331 | } |
| 332 | |
| 333 | var media = response && response.media && response.media[ 0 ]; |
| 334 | |
| 335 | if ( ! media || ! media.ID ) { |
| 336 | var message = response && ( response.message || response.error ); |
| 337 | wpFileError( fileObj, message || pluploadL10n.default_error ); |
| 338 | return; |
| 339 | } |
| 340 | |
| 341 | return stockUploadSuccess( fileObj, String( media.ID ) ); |
| 342 | }; |
| 343 | } ); |
| 344 | } )( jQuery ); |
| 345 |